An architectural pattern is a reusable way to slice a system so that change, failure, and scale land in predictable places. Use one when you can name the force you are optimizing for (independent deploys, a clear domain model, bursty I/O). It is not a GoF class diagram and not a UI pattern — MVC, MVVM, and MVP live in MVC, MVVM, and MVP.
Pattern vs style vs tactic
- Style is the vocabulary (client–server, REST, pipes and filters). See Architectural Styles.
- Pattern is a named structure with trade-offs (layered, hexagonal, microservices).
- Tactic is a local move (circuit breaker, cache). See Reliability Design Patterns.
Layered (n-tier)
Stack responsibilities so each layer only talks to neighbors: UI → application/use-case → domain → persistence/infrastructure. “n-tier” is the same idea when tiers are process or machine boundaries (browser, API host, database).
- Use when the team is small, the domain is one, and you want a boring default.
- Fails when every feature drills through all layers (“architecture sinkhole”) or the “business layer” becomes a dump of SQL and HTTP.
Hexagonal / ports and adapters
The domain sits in the middle. Ports are interfaces the domain owns (for example “load Order”). Adapters implement those ports (Postgres, REST, a test fake). Application services orchestrate use cases; they do not leak SQL types into the model.
Same family: onion and clean architecture — dependency arrows point inward. The win is testability and swapping infrastructure. The cost is more types up front; do not add a port for a one-off script.
Microservices
Independently deployable services, each with its own data and a clear bounded context. You buy independent scale and team autonomy. You pay with distributed failure, eventual consistency, and operational load.
Do not start here because the org chart might look like Amazon. Extract a service when a module already has a different rate of change, data owner, or failure domain. Pair with bounded contexts.
Event-driven
Components publish facts (“OrderPlaced”) instead of calling each other. A broker or bus routes events. Good for fan-out and temporal decoupling. Bad when you pretend events are a synchronous API (you now have an invisible call graph and poison messages). See Message Broker functions.
CQRS (when it is actually CQRS)
Split write model from read model when they disagree on shape or load. It is not “have a command handler class.” Most CRUD apps want one model. Add CQRS when the read side is a projection you can rebuild, not a second source of truth you edit by hand. CQRS is not event sourcing: that read model can be a projection of the same database.
Example
The use case names the port. The SQL adapter lives outside the domain project.
interface IOrders
{
void Add(Order order);
}
sealed class PlaceOrder
{
public void Handle(IOrders orders) => orders.Add(new Order());
}
sealed class SqlOrders : IOrders
{
public void Add(Order order)
{
// Postgres stays in this adapter, outside the domain project.
}
}
What breaks it
The domain method takes a DbContext, so every test and every new database drags Entity Framework in with it. Pass IOrders instead.
sealed class PlaceOrderThroughSql
{
public void Handle(DbContext db) => db.Add(new Order());
}
Pitfalls
- Copying a pattern’s boxes without the force it solves.
- Calling every folder “Clean Architecture” while the domain still depends on Entity Framework types.
- Microservices without ownership of data — that is a distributed monolith.