Choosing an Architecture
The Question That Matters#
Every team eventually faces the architecture question. Should we use hexagonal architecture? Clean architecture? Vertical slices? The discussion often devolves into abstract debates about principles and purity.
The right architecture for your team is not about which pattern sounds most impressive or which blog post made the strongest case. It is about matching your architectural choices to the actual forces your team faces. What changes most often in your codebase? Where does complexity accumulate? What causes your team to move slowly?
A younger version of me adopted clean architecture because it sounded professional, only to drown in boilerplate for simple CRUD operations. I have also avoided any structure at all because I valued speed, then ground to a halt as my codebase became an incomprehensible tangle. The architecture that works is the one that reduces friction for the changes you actually make, not the changes you imagine you might make someday.
This is not about learning every architectural pattern in depth. It is about understanding what each pattern optimizes for and whether those optimizations align with your reality. You need just enough knowledge to make an informed choice, then commit and build.
Vertical Slice Architecture#
Vertical slice architecture optimizes for speed of change when features evolve independently. It solves the problem of features becoming coupled through shared classes at each technical layer. Layered architectures encourage reusing the same controller, service, or repository across multiple features. This sharing creates coupling that makes changes risky. Modifying shared code to support one feature can break others in ways that are not obvious until something fails.
This pattern groups everything for one feature in one place. Each feature owns its complete implementation without sharing classes with other features. The code reveals business capabilities rather than technical abstractions. You understand what the system does by looking at folder names, not by reconstructing feature boundaries from scattered technical components.
Pros#
Changes stay fast and localized because features do not share classes. Each feature can evolve independently without affecting others. When you modify code for user registration, you have confidence it will not break order processing because they share nothing. You do not need to hunt through controllers folder, then services folder, then repositories folder to understand how one feature works. New team members can understand what the system does just by looking at the folder structure.
Requirements emerge through building, not upfront design. Features change more frequently than infrastructure. Domain understanding evolves as you ship and learn from users. Vertical slices optimize for this reality.
You can add complexity later when specific forces demand it. You cannot easily remove complexity once it is baked into your architecture.
Cons#
You may duplicate code across slices. This is often intentional, preferring duplication over coupling, but it requires discipline to avoid sharing state between features. Shared infrastructure like authentication and logging still needs coordination.
When to Choose#
Choose this when requirements emerge through building, features change frequently, domain understanding is evolving, and you need to move fast. This pattern optimizes for speed of iteration when you are still discovering what to build.
Hexagonal Architecture#
Hexagonal architecture optimizes for infrastructure flexibility by isolating business logic from external systems. It solves the problem of infrastructure changes cascading through your entire codebase. When swapping databases or adding new interfaces means rewriting business logic, your architecture couples decisions that should be independent.
Pros#
When you genuinely face infrastructure volatility, hexagonal architecture transforms that volatility from a system wide disruption into a localized change. Swapping databases becomes a matter of building a new adapter that satisfies the existing port contract. The rest of your application continues unchanged because it never knew about the database in the first place.
The pattern shines when testing isolation is your primary concern. Want to test business logic without touching a database? Mock the port. Want to verify adapter behavior without invoking the business logic? Test the adapter in isolation. This symmetric approach to external systems makes testing straightforward and fast.
Cons#
Hexagonal architecture adds real overhead. Every interaction with the outside world requires defining a port and building an adapter. For applications where infrastructure is stable, this overhead buys you nothing but extra code to maintain.
When to Choose#
You know from day one that you will swap out infrastructure components. Maybe you are building a proof of concept with SQLite but the production system will use PostgreSQL. Maybe you are starting with a REST API but know you will add a CLI and a message queue consumer soon. Maybe you work in an environment where database vendors change based on client contracts.
The key word here is “know.” You should have concrete evidence that infrastructure will swap, not speculation about what might happen someday. Choose hexagonal architecture when you have clear evidence of infrastructure volatility or when testing isolation justifies the abstraction cost.
Onion Architecture#
Onion architecture protects a stable domain core by placing business rules at the center with zero dependencies on anything external. It optimizes for domain longevity when infrastructure churns around stable business logic. This solves the problem of valuable domain knowledge getting tangled with framework code that becomes obsolete every few years.
Pros#
This inward dependency flow means you can swap out your entire infrastructure without touching business logic. Replace your web framework, change your database, switch your messaging system, the domain layer remains untouched because it never knew those things existed.
If you work in a highly regulated industry, your business rules might be mandated by law and remain stable for years while technology choices evolve. If you are building a system that needs to outlive multiple UI frameworks or serve the same business logic through constantly shifting interfaces, onion architecture provides the stability you need.
Cons#
The disadvantage is rigidity when domain rules themselves change. Because the domain is the foundation everything else builds on, changes to core business rules ripple outward through every layer. If your domain is experimental or rapidly evolving, this ripple effect creates friction exactly where you need fluidity.
When to Choose#
Choose onion architecture when you have proven that your domain is stable and infrastructure changes frequently. Do not choose it for experimental products or domains you are still discovering. The pattern optimizes for domain stability, which is a liability when domain itself is the thing that needs to change.
Clean Architecture#
Clean architecture optimizes for team independence at enterprise scale through strict layer boundaries and enforced dependency rules. It solves the problem of multiple teams breaking each other’s code in a shared codebase. When one team’s changes ripple unpredictably across team boundaries, coordination overhead consumes development capacity.
Pros#
This becomes relevant at enterprise scale. When you have multiple teams working on the same codebase, strict boundaries prevent one team’s changes from breaking another team’s work. When you need framework independence because frameworks will outlive their usefulness during your system’s ten year lifespan, clean architecture’s rigid layer separation protects you from framework lock in.
The pattern also makes sense when you have contractual requirements for framework independence. Some organizations mandate that business logic cannot depend on third party libraries for legal or compliance reasons. Clean architecture’s dependency rule enforces this mandate through structure.
Cons#
Clean architecture requires DTOs and mappers at every layer boundary. Data flows through the system gets transformed multiple times as it crosses from one layer to another. This is deliberate, it prevents layer coupling, but it creates substantial boilerplate. For simple applications, this overhead overwhelms any benefit.
The pattern also assumes you have a team fluent in these concepts. Clean architecture is not intuitive. It requires everyone to understand the dependency rule, the role of each layer, and why those boundaries matter. Without that shared understanding, the architecture becomes a source of confusion rather than clarity.
When to Choose#
Choose clean architecture when you have enterprise scale, long lifespan requirements, or strict framework independence mandates. Choose it when your team understands the pattern and has the discipline to maintain its boundaries. Do not choose it for startups, prototypes, or teams unfamiliar with the approach. The boilerplate burden is too high without clear enterprise justification.
The Questions That Guide You#
The right architecture reduces friction for the changes you actually make. Do detective work to discover where your friction lives.
What changes most often? If features change constantly, vertical slices keep changes localized by preventing features from sharing classes. If infrastructure swaps regularly, hexagonal architecture isolates that volatility. If domain rules stay stable while infrastructure churns, onion architecture protects your core.
Where does your team spend time? If developers waste hours navigating scattered files to understand one feature, you need better locality. If they fear changing shared code because it might break unrelated features, you need better boundaries. If they debate where new code belongs, you need clearer structure.
Match the architecture to the problem you observe, not the problem you imagine.