Architectural Styles
Comparison of major architectural patterns for structuring software systems. Each style manages dependencies differently and suits different contexts.
Layered Architecture (Classic)
┌─────────────────────┐
│ Presentation │ ← UI, controllers, API endpoints
├─────────────────────┤
│ Application │ ← Use cases, orchestration, services
├─────────────────────┤
│ Domain │ ← Business logic, entities, rules
├─────────────────────┤
│ Infrastructure │ ← DB, external APIs, frameworks
└─────────────────────┘
Rules: - Each layer depends only on layers below it - Domain layer has no external dependencies - Changes flow downward; domain is stable
Pros: Familiar, clear separation, easy to understand Cons: Database changes ripple up to domain; infrastructure leaks into business logic; hard to test domain in isolation
Best for: Simple CRUD apps, small teams, low domain complexity
Hexagonal Architecture (Ports & Adapters)
┌──────────────────┐
│ Primary Adapters │ ← REST, CLI, UI, gRPC
│ (Driving) │
└────────┬─────────┘
│ implements
┌────────▼─────────┐
│ Inbound Ports │ ← Use case interfaces
├──────────────────┤
│ Domain Core │ ← Entities, aggregates, domain services
│ (No deps out) │
├──────────────────┤
│ Outbound Ports │ ← Repository, notification, external service interfaces
└────────┬─────────┘
│ implemented by
┌────────▼─────────┐
│ Secondary Adapters │ ← DB, message queue, HTTP clients
│ (Driven) │
└──────────────────┘
Core rule: All dependencies point inward toward the domain core. The domain defines interfaces (ports); infrastructure implements them (adapters).
Key concepts: - Primary adapters (driving): Entry points — HTTP controllers, CLI, message consumers - Secondary adapters (driven): Exit points — database repos, external API clients, email senders - Ports: Interfaces owned by the domain, defining what it needs, not how
Pros: - Domain fully testable without infrastructure - Infrastructure swappable without touching domain - Aligns perfectly with Domain-Driven Design - Enables TDD — test domain via ports
Cons: More boilerplate; indirection can obscure flow; overkill for simple apps
Best for: Complex domains, long-lived systems, teams doing DDD, systems needing testability
Clean Architecture (Robert Martin)
┌─────────────────────────────────────┐
│ Frameworks & Drivers │ ← Web, DB, UI, Devices
├─────────────────────────────────────┤
│ Interface Adapters │ ← Presenters, Controllers, Gateways
├─────────────────────────────────────┤
│ Application Business │ ← Use Cases / Interactors
│ Rules │
├─────────────────────────────────────┤
│ Enterprise Business Rules │ ← Entities
└─────────────────────────────────────┘
Dependency Rule: Inner circles know nothing of outer circles. Dependencies point inward.
Differences from Hexagonal: - More prescriptive layer names - Use cases (interactors) are explicit layer between domain and adapters - Entities are innermost (enterprise-wide), use cases are application-specific
Pros: Very explicit boundaries; scream architecture (package by feature); good for large teams Cons: More layers = more mapping/boilerplate; can feel rigid
Best for: Large enterprise apps, regulated environments, teams needing strict boundaries
Onion Architecture (Jeffrey Palermo)
Similar to Clean but emphasizes: - Domain model at center (entities + domain services) - Application services orchestrate domain - Infrastructure at outer edge - Dependency inversion via interfaces in domain layer
Key insight: "The more you depend on things that change, the more you'll have to change."
Domain-Driven Design (DDD) — Strategic Architecture
DDD is not an architecture style per se, but it produces architectural patterns:
Bounded Contexts
Explicit boundaries within which a domain model is consistent and valid. - Different parts of a system can model "Customer" differently - Contexts communicate via Context Maps (conformist, anticorruption layer, shared kernel, etc.)
Context Map Patterns
| Pattern | Description |
|---|---|
| Shared Kernel | Subset of model shared explicitly; requires coordination |
| Customer/Supplier | Upstream/downstream relationship; downstream defines needs |
| Conformist | Downstream conforms to upstream model (no negotiation) |
| Anticorruption Layer | Translation layer protects downstream from upstream model |
| Open Host Service | Published protocol for multiple consumers |
| Published Language | Well-documented shared language (e.g., OpenAPI, Protobuf) |
Microservices Architecture
Not a style — an organizational/deployment pattern that uses architectural styles internally.
Each service typically uses: - Hexagonal/Clean internally - Own database (database per service) - Communicates via async messaging or synchronous API
Challenges: Distributed system complexity, data consistency, operational overhead, network latency
Best for: Large organizations, independent deployability needs, polyglot teams
Modular Monolith
A single deployable unit with internal modular boundaries (bounded contexts as modules).
┌────────────────────────────────────┐
│ Application │
├─────────┬─────────┬────────────────┤
│ Module A│ Module B│ Module C │ ← Bounded contexts as packages
│ (DDD) │ (DDD) │ (DDD) │
├─────────┴─────────┴────────────────┤
│ Shared Kernel │ ← Minimal shared code
└────────────────────────────────────┘
Pros: Simpler operations, easier refactoring, transactions across modules, no network latency Cons: Requires discipline to maintain boundaries; single failure domain
Best for: Most applications! Start here. Extract services only when organizational/scale demands it.
Comparison Matrix
| Concern | Layered | Hexagonal | Clean | Modular Monolith | Microservices |
|---|---|---|---|---|---|
| Testability (domain) | Low | High | High | High | High |
| Infrastructure swap | Hard | Easy | Easy | Medium | Easy |
| Boilerplate | Low | Medium | High | Low | High |
| Learning curve | Low | Medium | Medium | Low | High |
| Deploy simplicity | High | High | High | High | Low |
| Team scaling | Low | Medium | Medium | Medium | High |
| DDD alignment | Low | High | High | High | High |
Decision Guidance
| Situation | Recommended |
|---|---|
| Simple CRUD, small team, short timeline | Layered or Modular Monolith |
| Complex domain, long-lived, needs testability | Hexagonal / Clean |
| Multiple teams, independent deployment needs | Modular Monolith → Microservices |
| Regulated, audit-heavy, strict boundaries | Clean Architecture |
| Greenfield, uncertain domain | Modular Monolith (extract later) |
Key Principles Across All Styles
- Dependency inversion — Domain defines interfaces; infrastructure implements
- Separation of concerns — Business logic separate from infrastructure
- Boundaries align with change — Modules/bounded contexts = deployment/change units
- Explicit contracts — Ports, interfaces, APIs define integration points
- Testability as a design driver — If it's hard to test, the architecture is wrong
Related Notes
- Design Principles — Principles that inform these styles
- Domain Driven Design — DDD tactical/strategic patterns
- CQRS Event Sourcing — Advanced patterns for complex domains
- Architecture Decision Records — Document your architectural choices
- Integration Patterns — How services/modules communicate
- Team Topologies — Team structure should match architecture
Sources: AWS Prescriptive Guidance (Hexagonal), Clean Architecture (Robert Martin), Onion Architecture (Palermo), Domain-Driven Design (Evans), Software Engineering Guide, Martin Fowler, Microsoft Clean Architecture