JSTAcademy
0 XP
Dashboard
Software Engineering
Software Architecture Patterns
16 min
PhD+175 XP
Software Engineering · PhD

Software Architecture Patterns

MVC, hexagonal, event-driven, microservices — choosing the right architecture
16 min read+175 XP on completionCert: Software Engineering Discipline
Tap any word in the text below to start reading from there.

Software Architecture Patterns

Architecture is the set of decisions that are expensive to change: how the system is decomposed, how components communicate, where boundaries lie, and which concerns are separated. Good architecture maximizes the decisions you can defer.

Layered Architecture

The classic three-tier: Presentation → Application → Data. Each layer depends only on the layer below it. Simple, widely understood, and sufficient for many applications.

Problems at scale: the data layer bleeds into the application layer (domain objects are database entities), the presentation layer couples to specific APIs, and testing requires the full stack. The anemic domain model anti-pattern: domain objects are just data containers with no behavior, all logic in service classes.

Hexagonal Architecture (Ports and Adapters)

Alistair Cockburn's hexagonal architecture puts the application core at the center, with ports defining interfaces for everything external, and adapters implementing those interfaces.

Domain layer: pure business logic, no framework, no I/O, no database. Testable with zero infrastructure.

Application layer: orchestrates domain logic using ports. CreateOrderUseCase depends on IOrderRepository, IPaymentGateway, IEmailSender — all interfaces.

Infrastructure layer: adapters implementing ports — PostgresOrderRepository, StripePaymentGateway, SendgridEmailSender. Swappable without touching domain or application code.

This architecture makes the domain the most important part of the codebase (not the database) and enables testing domain logic in isolation. It underpins clean architecture (Robert Martin) and onion architecture.

Domain-Driven Design (DDD)

DDD (Eric Evans, 2003) aligns software design with the business domain:

Entities: objects with identity that persists through state changes. An Order is an entity — it has a unique ID; two orders with the same items are still different orders.

Value Objects: objects defined entirely by their attributes. Money(100, USD) equals Money(100, USD) — no identity needed. Immutable by definition.

Aggregates: clusters of entities and value objects with a clear boundary and root. An Order aggregate contains OrderItems, a ShippingAddress, and PaymentDetails. All access goes through the Order root. An invariant (order total must equal sum of items) is enforced by the aggregate.

Bounded Contexts: the Catalog context's "Product" has price, description, and image. The Inventory context's "Product" has stock level and warehouse location. These are different models sharing a name. Map their relationship in a Context Map.

Event-Driven Architecture and CQRS/ES

Event-Driven Architecture: components communicate through events. OrderService publishes OrderPlaced event; InventoryService consumes it to reserve stock; EmailService consumes it to send confirmation. Producers and consumers are decoupled.

Event Sourcing: instead of storing current state, store the full history of events. ORDER_PLACED → PAYMENT_RECEIVED → ITEM_SHIPPED → DELIVERED. Current state = replay from start. Benefits: complete audit trail, temporal queries ("what was the order state last Tuesday?"), event replay for migrations, projections.

CQRS: write side accepts commands and updates the domain model (append events). Read side maintains denormalized views (projections) optimized for queries. The read model is eventually consistent with the write model. This separates scaling concerns: reads can scale horizontally by adding read replicas; writes go through the aggregate.

Microservices

Microservices decompose a system into small, independently deployable services. Benefits: independent deployments, technology heterogeneity, failure isolation, team autonomy (Conway's Law: architecture mirrors org structure).

When to use microservices: when team size exceeds what can effectively own a monolith, when services have genuinely different scaling requirements, when deployment independence outweighs distributed systems complexity.

When not to: for a new system without clear domain boundaries (premature decomposition), for small teams (overhead of service mesh, distributed tracing, contract testing, deployment coordination exceeds the benefits).

The Strangler Fig pattern migrates a monolith to microservices: new functionality is built as separate services; existing monolith code is gradually replaced by extracting services.

Resilience Patterns

Circuit Breaker: wraps calls to remote services. After N consecutive failures, the circuit opens — calls fail immediately without attempting the network call. After a timeout, a probe request tests if the service recovered. Prevents cascade failures when one service is down.

Bulkhead: isolates thread pools or connection pools per dependency. If one downstream service consumes all threads, other services remain available. Named after ship compartments that prevent flooding the whole vessel.

Retry with Exponential Backoff: retry failed requests with increasing delays (1s, 2s, 4s, 8s) plus random jitter. Prevents thundering herd: if 1000 services all retry simultaneously after an outage, they immediately overwhelm the recovered service. Jitter staggers retries.

Timeouts: every remote call must have a timeout. Without them, a slow dependency can exhaust all threads.

Choosing an Architecture

For a new system with a single team: start with a monolith. Well-structured modular monolith with clear boundaries is far easier to develop, test, and deploy than microservices. Extract services only when you have clear scaling or team-ownership reasons.

For an existing system: add interfaces before extracting. Port and adapter boundaries in a monolith translate directly to service boundaries if you later extract — the code changes minimally, only infrastructure code changes.

0%