Application Architecture Patterns — Deep Dive

Level: Intermediate
Pre-reading: 01 · Architectural Foundations


The Core Idea: Dependency Inversion

All modern application architecture patterns share one principle: the domain/business logic should not depend on infrastructure concerns. Databases, HTTP, message queues — these are details that can change. Your business rules should be immune to such changes.

graph TD
    subgraph Wrong Direction
        A[Domain] --> B[Database]
        A --> C[HTTP Framework]
    end
graph TD
    subgraph Correct Direction
        D[HTTP Adapter] --> E[Domain Core]
        F[Database Adapter] --> E
        G[Message Queue Adapter] --> E
    end

Layered Architecture (N-Tier)

The traditional approach: stack layers on top of each other. Each layer can only call the layer directly below it.

graph TD
    A[Presentation Layer - Controllers] --> B[Business Layer - Services]
    B --> C[Persistence Layer - Repositories]
    C --> D[Database]
Layer Responsibility Spring Boot Example
Presentation HTTP handling, request/response mapping @RestController
Business Business logic, orchestration @Service
Persistence Data access, ORM @Repository, JPA
Database Storage PostgreSQL, MongoDB

Layered Architecture Problems

Problem Description
Tight coupling Business layer often imports persistence annotations
Database-driven design Entity model becomes the domain model
Testing difficulty Services need real or mocked repositories
Framework lock-in Spring/JPA annotations leak everywhere

The anemic domain model trap

In layered architecture, entities often become dumb data holders (getters/setters only), with all logic in services. This is an anti-pattern — business rules should live with the data they protect.


Hexagonal Architecture (Ports & Adapters)

Proposed by Alistair Cockburn. The domain core sits at the center, isolated from all external concerns via ports (interfaces) and adapters (implementations).

graph TD
    subgraph Adapters - Infrastructure
        A[REST Controller] 
        B[Kafka Consumer]
        C[PostgreSQL Repository]
        D[External API Client]
    end
    subgraph Ports - Interfaces
        E[OrderService Port]
        F[OrderRepository Port]
        G[PaymentGateway Port]
    end
    subgraph Domain Core
        H[Order Aggregate]
        I[OrderPlaced Event]
        J[PricingPolicy]
    end
    A --> E
    B --> E
    E --> H
    H --> F
    H --> G
    F --> C
    G --> D

Key Concepts

Concept Description
Port Interface defined by the domain; contract for interaction
Adapter Implementation of a port; connects to external systems
Driving adapter Initiates actions (HTTP, CLI, tests)
Driven adapter Called by the domain (DB, external APIs, messaging)

Hexagonal Architecture Benefits

Benefit How
Testability Test domain with in-memory adapters; no Spring context needed
Flexibility Swap PostgreSQL for MongoDB by writing a new adapter
Framework independence Domain has zero framework imports
Clear boundaries Ports define explicit contracts

Package Structure Example

com.example.order/
├── domain/
│   ├── Order.java              # Aggregate root
│   ├── OrderLine.java          # Entity
│   ├── Money.java              # Value object
│   ├── OrderPlaced.java        # Domain event
│   └── ports/
│       ├── OrderRepository.java    # Driven port (out)
│       ├── PaymentGateway.java     # Driven port (out)
│       └── OrderService.java       # Driving port (in)
├── application/
│   └── OrderServiceImpl.java   # Use case orchestration
├── adapters/
│   ├── in/
│   │   ├── rest/
│   │   │   └── OrderController.java
│   │   └── kafka/
│   │       └── OrderEventConsumer.java
│   └── out/
│       ├── persistence/
│       │   └── JpaOrderRepository.java
│       └── payment/
│           └── StripePaymentGateway.java

Onion Architecture

Proposed by Jeffrey Palermo. Concentric rings where dependencies always point inward. The domain is at the center; infrastructure is at the outermost ring.

graph TD
    subgraph Infrastructure Ring
        A[Controllers]
        B[Repositories Impl]
        C[External APIs]
    end
    subgraph Application Ring
        D[Use Cases]
        E[Application Services]
    end
    subgraph Domain Ring
        F[Entities]
        G[Value Objects]
        H[Domain Services]
    end
    A --> D
    B --> F
    D --> F
    E --> H
Ring Contents Dependencies
Domain Entities, Value Objects, Domain Services None (pure business logic)
Application Use cases, application services, ports Domain only
Infrastructure Controllers, DB adapters, external APIs Application + Domain

Onion vs Hexagonal

They're nearly identical. Onion makes the concentric dependency rule explicit and adds an Application layer for use case orchestration. In practice, teams use them interchangeably.


Clean Architecture

Proposed by Robert C. Martin (Uncle Bob). A formalization of hexagonal/onion with explicit use case layer and interface adapters.

graph TD
    subgraph Frameworks & Drivers
        A[Web Framework]
        B[Database]
        C[External Services]
    end
    subgraph Interface Adapters
        D[Controllers]
        E[Presenters]
        F[Gateways]
    end
    subgraph Application Business Rules
        G[Use Cases]
    end
    subgraph Enterprise Business Rules
        H[Entities]
    end
    A --> D
    D --> G
    G --> H
    G --> F
    F --> B

The Dependency Rule

Source code dependencies must point only inward, toward higher-level policies.

  • Entities know nothing about use cases
  • Use Cases know nothing about controllers or databases
  • Controllers know about use cases but not about database implementation

Clean Architecture Layers

Layer Responsibility Changes When
Entities Enterprise business rules Business rules change
Use Cases Application-specific business rules User workflow changes
Interface Adapters Convert data between layers UI or external API changes
Frameworks & Drivers Glue code for frameworks Framework version changes

Pipe and Filter Architecture

Data flows through a sequence of independent processing stages (filters) connected by pipes (channels).

graph LR
    A[Input] --> B[Filter 1 - Parse]
    B --> C[Filter 2 - Validate]
    C --> D[Filter 3 - Transform]
    D --> E[Filter 4 - Enrich]
    E --> F[Output]
Concept Description
Filter Stateless processing unit; single responsibility
Pipe Channel connecting filters; buffering, backpressure
Composability Reorder, add, or remove filters without affecting others

Real-World Examples

Domain Implementation
Data pipelines Apache Kafka Streams, Apache Flink
ETL Airflow DAGs, dbt transformations
Request processing Servlet filters, Spring interceptors
Unix philosophy cat file | grep error | sort | uniq

Comparing the Patterns

Pattern Key Distinction Best For
Layered Vertical stack; each layer calls the one below Simple CRUD apps
Hexagonal Ports and adapters; domain at center Testable, framework-agnostic systems
Onion Concentric rings; strict dependency direction Complex domains
Clean Explicit use case layer; UI-agnostic Enterprise applications
Pipe & Filter Linear data flow through processing stages ETL, streaming, request chains

Quick Decision Guide

Situation Recommendation
Simple CRUD, small team Layered is fine; don't over-engineer
Complex business logic Hexagonal/Onion — isolate domain
Multiple entry points (REST, CLI, events) Hexagonal — adapters per entry point
Emphasize testability Hexagonal/Clean — test domain in isolation
Data transformation pipeline Pipe and Filter

Common Implementation Mistakes

Mistake Impact Fix
Domain imports framework annotations Domain becomes untestable without framework Pure POJOs in domain; annotations in adapters only
Use case calls repository directly No clear boundary for domain logic Use case calls domain methods; domain uses repository port
Anemic entities Logic scattered across services Put behavior in entities with state
Too many layers Ceremony without benefit Start simple; add layers when complexity justifies them
One-to-one mapping between layers Entities = DTOs = database tables Each layer has its own model optimized for its purpose

What is the difference between Hexagonal, Onion, and Clean Architecture?

They all share the same core idea: domain at the center, dependencies pointing inward, infrastructure at the edges. Hexagonal emphasizes ports/adapters vocabulary. Onion visualizes concentric rings. Clean Architecture adds an explicit Use Case layer and is more prescriptive about layer responsibilities. In practice, they're interchangeable.

When is layered architecture acceptable?

For simple CRUD applications with little business logic, where the overhead of hexagonal/clean isn't justified. Also acceptable as a starting point for new teams — evolve toward hexagonal as complexity grows.

How do you test a hexagonal architecture application?

(1) Unit test the domain with in-memory implementations of driven ports (fake repositories, stub gateways). (2) Integration test adapters against real infrastructure (Testcontainers for DB). (3) End-to-end test through driving adapters (REST endpoints) with the full stack. The domain tests are fast and don't need Spring context.