Build your initial features quickly, then evolve your software based on customer feedback. This demands having a good suite of tests to prevent cruft and promote refactoring without the fear of breaking those features. This also requires being able to recognize design smells. These smells will permeate your software due to bad decisions caused by carelessness and false expedience. Manage the dependencies within your software to ensure a loosely coupled architecture and to isolate high-level policies from low-level detail.
Software functionality should progressively gain maturity and viability. Start with the simplest solution that meets the customers’ needs, then evolve that solution toward a mature product. Be careful not to over-engineer the initial design (YAGNI) and avoid introducing Technical Debt. Know what bad designs smell like and avoid them as much as possible. And if you are going to introduce Technical Debt, make it visible in your backlog, ensure all stakeholders understand the impact, and develop a plan to pay it down quickly. Here are several design smells that should be well understood and avoided:
A Fragile system is one that breaks easily. This is often observed when a change to one module causes other unrelated modules to break. Change one thing over here and break five others over there. Bob Martin describes a good example regarding the software that controls an automobile... That software would be fragile if when you fixed a bug to the radio, it affected the electric windows. I have seen this occur many times in business applications. And how do you think the business partners regard the developers in that situation? Like cottonheaded ninnymuggins who don't have a clue as to what they're doing? Yeah, pretty much.
A Rigid system is one that is hard to change. This is often observed when a small change forces a complete rebuild and redeploy. Small changes should be able to be built, tested, and deployed very quickly and independently of each other. Long build times are a symptom of high coupling. To promote flexibility, we must manage the dependencies between modules to ensure when one module changes, the others remain unaffected.
An Immobile system is one from which it is difficult to extract and reuse internal components in new environments which is often caused by tightly coupled code. For example, a user login module would be immobile if it depended on a particular database schema that was not consistent across other systems. To promote mobility, that module should be decoupled from low-level implementations, such as data persistence, logging, user interfaces, etc.
A Needlessly Complex system is one which obscures its intent, contains duplication, and provides hooks in anticipation of future extensions. If your code is rigid, then you're likely to add a lot of anticipatory design elements to minimize the amount of design changes you will need to make later.
A Viscose system is one in which development activities (such as building, testing, and deploying) are difficult to perform and take a long time to execute. System designs in which components span multiple layers of the system are often viscose, because even the simplest change is costly to make. And it all comes down to decoupling our systems through dependency management.