Dualo
System Design Essentials

Architecture patterns — monolith, microservices, CQRS, event sourcing

The big four architectural decisions. Pick by team size, domain complexity, and scale — not by hype.

2 min read

**Monolith pros**: single runtime (debugger attaches to everything), single DB (cross-entity transactions trivially ), simpler deploy pipeline, lower observability overhead, smaller cognitive load. Monolith cons: team-scale bottleneck (merge conflicts, slow tests, all-or-nothing deploys), scaling limitation (vertical only, or horizontal at coarse grain), tech lock-in (the whole codebase shares language/stack).

Modular monolith patterns: enforce module boundaries via (i) separate top-level packages with explicit APIs; (ii) per-module DB schema (no cross-schema direct reads); (iii) internal events/commands between modules (no direct function calls between domains). Tools: Spring Modulith (Java), NestJS modules, Nx monorepo with ESLint boundaries. Essentially 'microservice discipline without the network'.

Microservices trade-offs: each service owns its data (essential — shared DB = coupling nightmare). Consequences: no cross-service ACID (use saga pattern or eventual consistency), must version APIs as contracts, deployment orchestration (k8s / ECS / Nomad), service discovery + retry + timeouts + circuit breakers, centralized observability for debugging distributed flows. Operational cost ≈ 2-3× monolith for small teams.

CQRS in practice: write-side processes commands → emits events → read-side subscribers update denormalized query views (in Postgres, Elasticsearch, Redis, etc.). Read and write can be different stores. Eventual consistency between them (milliseconds usually). Heavy pattern — justified when read/write ratios are very skewed (1000× more reads) or when read models differ wildly.

Event sourcing specifics: event log is the source of truth (append-only); current state is a projection. Events are immutable (never modify past events). Must handle: (i) event schema evolution (additive changes + upcasting); (ii) snapshots for performance (don't replay million events on every read); (iii) projection rebuild when projection logic changes. Frameworks: EventStoreDB, Axon, Marten.

**** (for distributed transactions): instead of 2PC, model a multi-step business transaction as a sequence of local transactions with compensating actions for rollback. Choreography (events trigger next step) vs orchestration (central saga coordinator). Examples: booking a flight + hotel + car; if hotel fails, compensate flight (cancel). Tools: Temporal, AWS Step Functions, Camunda.

Circuit breaker state machine: closed (normal — count failures); open (refuse calls, return error/fallback immediately — skip the failing dependency); half-open (let one test call through after cooldown; if success → closed, if failure → open). Libraries: Resilience4j (Java), Polly (C#), Go-kit breaker, Istio DestinationRule-outlier-detection.

Bulkhead pattern: isolate failure domains by pool (separate connection pool for auth service vs billing service → one saturation doesn't starve the other). Named after ship compartments that contain flooding.

API Gateway pattern: single entry for client traffic, handles cross-cutting concerns (auth, rate-limit, request routing, response aggregation). Tools: Kong, Envoy, AWS API Gateway, Cloud Endpoints, Apigee. Overkill for monolith; essential once 5+ microservices.

****: sidecar proxies (Envoy, Linkerd) injected alongside each service handle networking concerns (mTLS, retries, circuit breaker, metrics, tracing) without in-app changes. Centralized config (Istio, Linkerd2, Consul Connect). Adds complexity; usually needed only past a certain microservice count + compliance requirements.

Grounded on https://martinfowler.com/microservices/