I tend to have the opposite problem. I'm a bit of a perfectionist and tend to optimize prematurely. And it's sometimes difficult to convince clients that a little bit of extra work upfront will pay off and save them a lot of time and money in the long run, especially when building something that is expected to scale and evolve over time.
I usually do dev work solo or with a small team of 2-3 people, so I've had to deal with these things in a very mindful and deliberate manner. To get more into specifics, here are some things I've learned:
- I think about scope and complexity at all times, before, during, and after development. I try to anticipate issues that might arise and act preemptively, never letting things get out of hand. Writing code and setting up the infrastructure around it is sort of like building a house. You need a blueprint and a well thought out plan, but you also need to monitor the process to make sure everything is done properly and leading to the desired outcome (no matter how variable that outcome may be).
- I use every technique I know and tool I have to make my code as clean and modular as possible. From IDE shortcuts and syntax formatters, to SOLID principles and design patterns. With the latter, you need to know when to apply them and how to do it in order to extract the most value without over-engineering stuff. Needless to say, duct tape has never been in my toolbox, and if I see someone else using it, I have an urge to peel it off and throw it away.
- Understanding different techniques for structuring your code is important. I've found that a variation of the package-by-feature (or package-by-component) approach works best for me in most scenarios. And no, this is not only useful for backend OOP stuff, but for frontend JS code (e.g., React) as well. For web backends, I usually have a package for each domain (e.g. user, product, order, etc.) and keep them self-contained and independent from each other. A package contains everything that belong to a domain entity/event or feature; all related data access logic, business logic, data transfer objects, routing/controllers, etc. are in one place. At the top level, I sometimes have a package for each architectural layer (e.g., infrastructure, application, domain, etc.) with minimal or no dependency on other packages from the same layer. Throw in dependency injection, and I can easily swap out entire modules and test every single thing in isolation.
- Speaking of testing, when time and resources allow, using a combination of unit, integration, and end-to-end tests to cover the most essential features and functionality can be useful in large projects, preventing bugs and breaking changes.
- Identifying the need for architectural change, e.g., breaking things up into microservices, or using a specific architectural pattern (e.g., event-driven architecture) is also important if you're dealing with large and/or data-heavy systems.
- To visualize the structure and dependencies of your code, you can use a variety of tools, either external or built into your IDE. I've never had to use anything beyond the built-in ones — these diagrams are much more useful to have before you start writing code.