Software Architecture Patterns Compared: Monolith, Microservices, Event-Driven, Hexagonal, and More

Architecture choice is the decision with the longest tail. A framework you pick today can be swapped in a weekend; an architecture you commit to shapes how your team organizes, how you deploy, how you debug, and how you scale — for years. Yet most teams choose an architecture by default: they pick what they already know, what the framework suggests, or what a high-profile tech company published a blog post about.

This article compares the six most widely used architecture patterns — Monolith, Layered, Microservices, Event-Driven, Hexagonal, and Serverless — on concrete dimensions: operational cost, team fit, testability, scalability, and failure modes. The goal is not to declare a winner but to give you the vocabulary and trade-off map to choose deliberately.

At a Glance

Complexity Scalability Testability Ops cost Team fit
Monolith Low Medium High Low Any size
Layered Low Medium Medium Low Small–medium teams
Microservices High High Medium High Large, multi-team
Event-Driven High Very High Low Medium Async-first teams
Hexagonal Medium Medium Very High Medium DDD-fluent teams
Serverless Medium Very High Medium Low Small teams, spiky load
Relative ratings — all trade-offs depend on team size, load profile, and domain complexity

1. Monolithic Architecture

A monolith is a single deployable unit where all components — UI, business logic, data access — live in the same codebase and are deployed together. This is not a dirty word. Most successful software starts as a monolith, and many systems are best served by staying as one.

HTTP / API
↓
Business Logic
↓
Data Access
↓
Database
single deployable unit
All layers coexist in one process — one build, one deploy, one failure domain

When it wins: Early-stage products, small teams (under ~8 engineers), domains where the full context is needed to make decisions, and applications where sub-millisecond internal calls matter. A monolith has zero network overhead between components — that's a real advantage for certain workloads.

Where it fails: When deployment frequency becomes a bottleneck — if deploying one feature risks all features, teams slow down. Also when different parts of the system have radically different scaling requirements: if your search feature needs 10x the compute of your billing feature, you can't scale them independently.

The modular monolith: The middle ground most teams skip. A codebase organized into clear, bounded modules with enforced dependency rules — still one deployable, but with the internal structure that makes future extraction tractable. This is often the right starting point that microservices advocates forget to mention.

2. Layered (N-Tier) Architecture

Layered architecture organizes code into horizontal layers — typically Presentation, Application, Domain, and Infrastructure — where each layer only depends on the layer below it. It's the most commonly taught architecture and the default structure for most frameworks (MVC is a three-layer pattern).

src/ ├── presentation/ # HTTP handlers, request/response DTOs │ └── controllers/ ├── application/ # Use cases, orchestration, no domain knowledge │ └── services/ ├── domain/ # Entities, value objects, domain rules — pure, no I/O │ ├── models/ │ └── repositories/ # interfaces only └── infrastructure/ # DB, external APIs, email, queue — implements domain interfaces └── persistence/

The key rule: dependencies point downward only. Presentation depends on Application; Application depends on Domain; Domain depends on nothing; Infrastructure implements Domain interfaces. Violating this — a domain entity directly importing an ORM model, for instance — is the most common way layered architecture degrades into an unmaintainable codebase.

When it wins: CRUD-heavy applications, teams onboarding new engineers (the structure is familiar), or applications where the domain logic is not complex enough to justify hexagonal architecture's ceremony.

Where it fails: The "database layer" often ends up leaking into the domain — ORM entities with business logic, or domain models that are just ActiveRecord objects. The separation looks clean in theory but requires active enforcement. Without it, the layers become cosmetic.

3. Microservices Architecture

Microservices decompose a system into small, independently deployable services — each owning a bounded domain context, its own database, and its own deployment pipeline. Services communicate over the network via HTTP/REST, gRPC, or asynchronous messaging.

API Gateway
auth · routing · rate limit
User Service
PostgreSQL
Order Service
MySQL
Payment Service
PostgreSQL
Notification Svc
Redis / Queue
Catalog Service
Elasticsearch
Each service owns its data store — no shared databases, communication via network only

When it wins: Large organizations where multiple teams need to ship independently without coordinating deploys. When different services have radically different scaling needs. When the domain is well-understood enough that service boundaries can be drawn correctly — getting boundaries wrong is far more costly in microservices than in a monolith.

Where it fails: Distributed systems are genuinely harder. Every synchronous call across a network introduces latency, timeouts, and partial failures. Data consistency across services requires eventual consistency patterns (Saga, Outbox) that add significant complexity. Teams that migrate to microservices without operational maturity (service mesh, distributed tracing, circuit breakers) routinely end up with systems that are harder to debug than the monolith they left.

The two-pizza rule is not an architecture rule: Amazon's "team that can be fed by two pizzas" is about team autonomy, not service granularity. Services should be sized by domain boundary, not by team headcount. Nano-services (one endpoint per service) are a common microservices failure mode — the overhead outweighs every benefit.

4. Event-Driven Architecture

Event-Driven Architecture (EDA) structures communication around events rather than direct calls. Producers emit events; consumers subscribe and react. Neither side knows about the other — the event broker (Kafka, RabbitMQ, AWS SNS/SQS) is the only shared contract.

Order Service
Payment Service
emit events
→
Kafka / RabbitMQ
event broker
consume events
→
Notification Svc
Analytics Svc
Fulfillment Svc
Producers and consumers are fully decoupled — adding a new consumer requires zero changes to the producer

When it wins: High-throughput systems where producers shouldn't block waiting for consumers (payment confirmation triggers email, analytics, inventory update — all independently). Systems that need event replay for audit or recovery. Fan-out scenarios where one event triggers many downstream processes.

Where it fails: Testability drops sharply — testing an async flow requires either a test broker or careful mocking of the entire pipeline. Debugging requires distributed tracing to follow a request across multiple consumers. Event schema evolution is difficult: once producers publish an event shape, changing it can break consumers silently. And eventual consistency is a constraint, not just a property — user-facing flows that require immediate consistency are awkward to implement.

5. Hexagonal Architecture (Ports & Adapters)

Hexagonal architecture, introduced by Alistair Cockburn, inverts the dependency direction of traditional layered architecture. The domain sits at the center and depends on nothing — not on the database, not on the web framework, not on external APIs. Everything external (HTTP, DB, message queue, CLI) connects through ports (interfaces defined by the domain) and adapters (implementations of those interfaces).

Domain
Pure logic
no I/O
HTTP Adapter
DB Adapter
Queue Adapter
Email Adapter
Domain at the center depends on nothing — adapters implement the ports the domain defines

When it wins: Complex business domains where the logic must be testable in isolation — no database, no HTTP, no external services. Teams practicing DDD benefit most: the domain model is clean enough to be tested with simple unit tests, and swapping infrastructure (switching ORMs, adding a new delivery channel) doesn't touch the domain.

Where it fails: The upfront structure is heavier than layered architecture — more interfaces, more files, more indirection. For CRUD-heavy applications with little business logic, the ceremony is disproportionate. Teams unfamiliar with dependency inversion often implement hexagonal architecture incorrectly, ending up with the complexity without the benefit.

6. Serverless Architecture

Serverless pushes deployment to its extreme: individual functions deployed to a provider (AWS Lambda, Google Cloud Functions, Azure Functions) that handles all infrastructure. You pay per invocation, scale to zero automatically, and have no servers to manage.

// A complete serverless "service" — deployed and scaled independently export const handler = async (event: APIGatewayProxyEvent) => { const orderId = event.pathParameters?.id; const order = await db.orders.findById(orderId); return { statusCode: 200, body: JSON.stringify(order) }; }; // Infrastructure is the provider's concern — no EC2, no Kubernetes, no Nginx

When it wins: Spiky, unpredictable workloads where paying for idle capacity is wasteful. Scheduled tasks, webhook processors, event consumers, and API backends with variable load. Small teams that want to ship without managing infrastructure. The operational floor is genuinely low — AWS handles autoscaling, patching, and availability.

Where it fails: Cold starts add latency for the first invocation after idle periods — unacceptable for latency-sensitive user-facing APIs. Function size and duration limits constrain what can run serverless. Vendor lock-in is real: a function tightly coupled to AWS event schemas is expensive to migrate. And stateless-by-design means any statefulness (sessions, in-flight workflows) must be externalized — usually to DynamoDB or Redis, adding round-trips.

7. CQRS: Separating Reads from Writes

Command Query Responsibility Segregation (CQRS) is not a full architecture — it's a pattern applied within one. The core idea: the model you use to write data is different from the model you use to read it. Commands (create, update, delete) go through the write side with full validation and domain logic. Queries go through a separate read model optimized purely for display — often a denormalized projection.

Client
Command →
Write Model
Domain logic · Validation
→ Write DB
← Query
Read Model
Flat projections · No domain logic
← Read DB (denormalized)
Write DB stays normalized and consistent; Read DB is a pre-computed projection optimized for queries — synced asynchronously

When it wins: Systems where read and write workloads have very different shapes — e.g., an order processing system where writes are complex (20-step domain flow) but reads serve dashboards with aggregated views. CQRS lets you optimize each side independently: the write model enforces invariants; the read model serves flat, fast projections. It pairs naturally with Event Sourcing, where each command produces an event that updates read projections.

Where it fails: Simple CRUD applications — the split adds significant complexity for no benefit when your reads and writes look identical. Eventual consistency between the write DB and read projections can confuse users who expect to see their write reflected immediately. Start with a single model; introduce CQRS only when you can clearly articulate the asymmetry it solves.

Migrating Between Architectures: The Strangler Fig

The most common question isn't "which architecture should I choose for a greenfield project?" — it's "how do I migrate from what I have?" The Strangler Fig pattern, coined by Martin Fowler, is the standard answer. The pattern takes its name from a plant that grows alongside a tree, gradually replacing it without ever needing to cut the original down.

Phase 1
Add a Façade in front of the monolith. New feature routes go to the new service. Old routes pass through.
→
Phase 2
Extract domain by domain. Each extraction moves a route from the monolith to a new service. Monolith shrinks.
→
Phase 3
Monolith handles only remaining legacy routes. Decommission when the last route is migrated.
The monolith stays live and serving traffic throughout — zero "big bang" cutover, rollback is always possible

The critical discipline: never extract a service before you understand the domain boundary. The most common failure in Strangler Fig migrations is extracting services before the domain is well-defined, then discovering the boundary was wrong only after the extraction is complete. Extract opportunistically — when a new feature must be built anyway, build it as a service instead of adding to the monolith.

How to Choose: A Decision Framework

Architecture choice should be driven by three questions about your specific context — not by trends, not by what Netflix or Uber published, and not by what you already know:

1. How big is the team, and how do you need to organize?
1–5 engineers → Monolith or Serverless. 5–20 engineers → Modular monolith or Layered. 20+ engineers across independent teams → Microservices is justified. Conway's Law is real: your architecture will mirror your team structure regardless.
2. How complex is the domain?
Simple CRUD with few business rules → Layered is enough. Complex business rules with rich invariants → Hexagonal architecture pays off. If you can't clearly articulate your domain boundaries yet, don't do microservices — you'll get them wrong and pay for it.
3. What is your load profile and operational maturity?
Steady load, known scale → Scale the monolith vertically first. Spiky or unpredictable load → Serverless or Event-Driven for the hot paths. High operational maturity (SRE team, service mesh, distributed tracing) → Microservices or EDA. No dedicated SRE team → Microservices will hurt you.
Team size, domain complexity, and operational maturity — these three factors determine architecture fit more than any trend

Architectures Are Not Mutually Exclusive

Production systems rarely use one architecture cleanly. A common and pragmatic combination: a modular monolith for core business logic (Hexagonal internally, single deployment), event-driven integration with external systems (Kafka for async workflows), and Serverless for peripheral workloads (scheduled reports, image processing, webhook ingestion). Each part of the system uses the architecture that fits its specific constraints.

The migration path matters: The best architecture for today is the one you can migrate from when your context changes. A well-structured monolith with clear module boundaries can be extracted to microservices service by service. A monolith with no boundaries is a distributed system waiting to be a nightmare. Start with the simplest architecture that meets your current needs, but build it with future extraction in mind.

Key Takeaways

Back to all articles