Design patterns often feel forced when applied at the code level. The real clarity comes when you step back, map your use-cases, and work from the right level of abstraction.
In my previous blog on The Sneaky Distributed Monolith, I reflected on how monstrous spaghetti code often sneaks up on us unnoticed. Spaghetti code is the natural byproduct of entropy and avoiding it is an active process. I mentioned that the remedy lies in careful planning and disciplined, iterative development. However, planning alone is not always enough. Even with good intentions, it’s not always obvious where to place functionality or when to apply certain practices.
One of the biggest aids in producing well-organized and maintainable code are the SOLID principles and design patterns. Yet, when I first learned them, I found myself asking: Where do I actually apply these patterns? Too often, it felt like I was trying to sneak them in for the sake of ticking a box rather than because they naturally fit the design.
The key realization I had later was this: the level of abstraction you use in your design makes all the difference. Most design patterns don’t reveal their usefulness when you’re staring at raw source code. They start to make sense when you zoom out and view your application from a bird’s eye perspective. At this level, you can trace the different use-case flows, not in painstaking detail, but enough to see how they interact.
This higher level view helps you spot common flows across multiple use-cases, which is the right place to extract shared functionality, introduce interfaces, or apply other design patterns.
When you model your analysis classes, you begin to see how your use-cases overlap, which entities they share, and where controllers may serve more than one scenario. This is the ideal stage to consider refactoring while still avoiding unnecessary coupling. By the time you refine into design classes and finally move into implementation, you already have a well-structured scaffolding for your codebase, one that makes patterns feel natural instead of forced.
Of course, new functionality will always be needed. The question is: Where should it live?
Whenever you add features, return to your use-case diagrams and domain entities. Ask yourself:
Starting from the high-level design ensures that each new piece of functionality ends up where it belongs. It also naturally highlights opportunities to apply design patterns that will serve you well in the long run.