Cheat SheetsSystem DesignArchitectural Patterns

Architectural Patterns — Cheat Sheet

System Design · 7 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Architectural Patterns
System Design7 topicsQuick revision reference
1

Microservices Architecture

Microservices decompose a system into small, independently deployable services, each owning a specific business capability. They enable team autonomy, independent scaling, and technology flexibility at the cost of distributed-systems complexity.

  • Each microservice owns one business capability and its own database.
  • Services communicate via REST/gRPC (sync) or events/queues (async).
  • Benefits: independent deployment, team autonomy, per-service scaling, tech flexibility.
  • Costs: distributed transactions, debugging complexity, network overhead, operational burden.
  • Start with a monolith; extract microservices when team size and scale justify the complexity.
Conceptual — monolith vs microservices
// Monolith — single deployable unit
// ┌─────────────────────────────────┐
// │           Monolith               │
// │  ┌──────┐ ┌──────┐ ┌──────┐    │
// │  │Users │ │Orders│ │Pay   │    │
// │  │Module│ │Module│ │Module│    │
// │  └──┬───┘ └──┬───┘ └──┬───┘    │
// │     └────────┴────────┘         │
// │         Shared Database         │
// └─────────────────────────────────┘

// Microservices — independent services
// ┌──────────┐  ┌──────────┐  ┌──────────┐
// │ User Svc  │  │ Order Svc │  │ Pay Svc   │
// │ (Java)    │  │ (Java)    │  │ (Go)      │
// └─────┬────┘  └─────┬────┘  └─────┬────┘
//       │             │             │
//   Users DB      Orders DB     Payments DB
//  (PostgreSQL)  (PostgreSQL)    (DynamoDB)

// Communication:
// Sync: REST / gRPC (request-response)
// Async: Kafka / SQS (event-driven)
2

CQRS (Command Query Responsibility Segregation)

CQRS separates read (query) and write (command) models into different data stores optimised for each. Writes go to a normalised DB; reads come from a denormalised, pre-computed view.

  • CQRS separates write (command) and read (query) into different models and stores.
  • Write model: normalised, enforces business rules. Read model: denormalised, optimised for queries.
  • Events propagate changes from write to read side — eventual consistency.
  • Enables independent scaling: more read replicas for high-read workloads.
  • Use CQRS when read and write patterns are vastly different — not for simple CRUD apps.
Conceptual — CQRS architecture diagram
// CQRS architecture
//
//  Client
//   │ Command (write)          │ Query (read)
//   ▼                          ▼
// ┌──────────────┐        ┌──────────────┐
// │ Command Side  │        │  Query Side   │
// │ (Write Model) │        │ (Read Model)  │
// │ OrderService   │        │ OrderView     │
// └──────┬───────┘        └──────▲───────┘
//        │ write                  │ read
//        ▼                        │
//  ┌──────────┐  event    ┌──────────┐
//  │ Orders DB │ ────────►│ Read DB   │
//  │(PostgreSQL)│ (Kafka)  │(Elasticsearch)│
//  └──────────┘           └──────────┘
//  (normalised)           (denormalised, fast queries)

// Write model: normalised, enforces business rules
// Read model: denormalised, optimised for UI queries
// Event bus: propagates changes from write → read
3

Event Sourcing

Event sourcing stores the state of a system as a sequence of immutable events rather than mutable rows. Current state is derived by replaying events. It provides a complete audit trail and enables temporal queries.

  • Event sourcing stores all state changes as immutable events — current state is derived by replay.
  • Provides complete audit trail, temporal queries, and natural integration with event-driven systems.
  • Snapshots prevent performance degradation from replaying long event histories.
  • Projections build read-optimised views from events — often combined with CQRS.
  • Event schema evolution is the biggest long-term challenge — plan for versioning from day one.
Conceptual — event store vs traditional state
// Traditional DB — stores current state
// | account_id | balance |
// |     A1     |  500.00 |
// Previous values are lost on UPDATE

// Event sourcing — stores all changes as events
// Event Store:
// | event_id | aggregate_id | type         | data              | timestamp   |
// | 1        | A1           | Created      | { owner: "Alice" }| 2025-01-01  |
// | 2        | A1           | Deposited    | { amount: 1000 }  | 2025-01-05  |
// | 3        | A1           | Withdrawn    | { amount: 300 }   | 2025-01-10  |
// | 4        | A1           | Deposited    | { amount: 200 }   | 2025-02-01  |
// | 5        | A1           | Withdrawn    | { amount: 400 }   | 2025-03-01  |
//
// Current balance = replay: 0 + 1000 - 300 + 200 - 400 = 500
// Balance on Jan 10? Replay events 1-3: 0 + 1000 - 300 = 700
4

Saga Pattern

The Saga pattern manages distributed transactions across microservices using a sequence of local transactions with compensating actions for rollback, replacing traditional two-phase commit.

  • Saga replaces distributed transactions (2PC) with a sequence of local transactions + compensating actions.
  • Choreography: services react to events (decentralised, good for simple sagas).
  • Orchestration: central coordinator manages steps (better for complex multi-step sagas).
  • Compensating transactions must be idempotent — they may be triggered multiple times.
  • Sagas provide eventual consistency, not ACID — design for intermediate states being visible.
Conceptual + Java — choreography saga
// Choreography saga — order placement
//
// 1. OrderService: create order (PENDING) → emit "OrderCreated"
// 2. PaymentService: listens "OrderCreated" → charge payment → emit "PaymentCompleted"
// 3. InventoryService: listens "PaymentCompleted" → reserve stock → emit "StockReserved"
// 4. OrderService: listens "StockReserved" → update order to CONFIRMED
//
// Failure at step 3 (out of stock):
// 3. InventoryService: emit "StockReservationFailed"
// 2. PaymentService: listens → refund payment → emit "PaymentRefunded"
// 1. OrderService: listens → cancel order → set status to CANCELLED

// Spring Boot choreography implementation
@KafkaListener(topics = "order-created")
public void onOrderCreated(OrderCreatedEvent event) {
    try {
        paymentGateway.charge(event.getUserId(), event.getTotal());
        kafka.send("payment-completed", new PaymentCompletedEvent(event.getOrderId()));
    } catch (PaymentFailedException e) {
        kafka.send("payment-failed", new PaymentFailedEvent(event.getOrderId()));
    }
}
5

API Gateway

An API Gateway is a single entry point for all client requests. It handles routing, authentication, rate limiting, request aggregation, and protocol translation, shielding internal microservices from direct client access.

  • API Gateway is a single entry point — routes, authenticates, rate-limits, and aggregates.
  • Decouples clients from internal service topology — clients call one URL, gateway routes internally.
  • Backend for Frontend (BFF): separate gateways tailored for mobile, web, and third-party clients.
  • Popular options: Kong, NGINX, AWS API Gateway, Spring Cloud Gateway, Envoy.
  • Avoid making the gateway a bottleneck — keep it thin, push business logic to services.
Conceptual — API gateway responsibilities
// API Gateway architecture
//
//  Mobile App    Web App    3rd-party
//       │           │          │
//       └─────┬─────┴──────────┘
//             ▼
//     ┌──────────────────┐
//     │   API Gateway     │  ← routing, auth, rate limit, aggregation
//     │  (Kong / NGINX)   │
//     └──┬──┬──┬──┬──────┘
//        │  │  │  │
//        ▼  ▼  ▼  ▼
//    User  Order  Payment  Search
//    Svc   Svc    Svc      Svc

// Gateway responsibilities:
// 1. Routing:      /api/users/** → User Service
// 2. Auth:         Validate JWT, attach user context
// 3. Rate limit:   100 req/min per API key
// 4. Aggregation:  /api/dashboard → calls 3 services, merges response
// 5. Transformation: REST → gRPC (for internal services)
// 6. SSL termination: HTTPS at gateway, HTTP internally
// 7. Logging:      Centralised access logs, request tracing
6

Service Discovery

Service discovery enables services to find each other dynamically in a microservices environment where instances scale up/down and IPs change frequently. It replaces hardcoded addresses with a registry-based lookup.

  • Service discovery replaces hardcoded addresses with dynamic registry-based lookup.
  • Client-side discovery: client queries registry and load-balances (Eureka + Ribbon/Feign).
  • Server-side discovery: load balancer or DNS resolves service names transparently (Kubernetes Services).
  • Kubernetes provides built-in service discovery via DNS — no external registry needed.
  • Health checks ensure only healthy instances are discoverable.
Conceptual + K8s YAML — discovery patterns
// Client-side discovery (Eureka + Spring Cloud)
// 1. Service registers itself with Eureka
// 2. Client queries Eureka for "ORDER-SERVICE"
// 3. Client gets list of IPs: [10.0.1.5:8080, 10.0.1.6:8080]
// 4. Client picks one (round-robin, random, etc.)

// Server-side discovery (Kubernetes)
// 1. Pod registers via Kubernetes API (automatic)
// 2. Client calls: http://order-service:8080/api/orders
// 3. kube-proxy resolves DNS → one of the pod IPs
// 4. Transparent to the client

// Kubernetes Service (server-side discovery)
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service     # matches pods with this label
  ports:
    - port: 8080
      targetPort: 8080
  type: ClusterIP          # internal DNS: order-service.default.svc.cluster.local
7

Strangler Fig Pattern

The Strangler Fig pattern incrementally migrates a monolith to microservices by gradually routing traffic from legacy endpoints to new services, eventually "strangling" the old system.

  • Strangler Fig enables incremental monolith-to-microservices migration without big-bang rewrite.
  • A proxy/gateway routes traffic — new endpoints go to microservices, rest stays on monolith.
  • Each migrated endpoint can be rolled back to the monolith if issues arise.
  • Database splitting is the hardest part — use CDC (Debezium) for transition period sync.
  • Combined with feature flags and canary deployments for low-risk cutover.
Conceptual + NGINX — strangler fig migration
// Strangler Fig migration phases
//
// Phase 1: Proxy in front of monolith (everything goes to monolith)
// Client → API Gateway → Monolith (all endpoints)
//
// Phase 2: Extract UserService
// Client → API Gateway
//   /api/users/**  → NEW UserService (microservice)
//   /api/*         → Monolith (everything else)
//
// Phase 3: Extract OrderService
// Client → API Gateway
//   /api/users/**  → UserService
//   /api/orders/** → NEW OrderService
//   /api/*         → Monolith (shrinking)
//
// Phase N: Monolith is empty → decommission
// Client → API Gateway → All microservices

// NGINX routing during migration
upstream monolith { server monolith:8080; }
upstream user_service { server user-svc:8080; }
upstream order_service { server order-svc:8080; }

server {
    location /api/users/ { proxy_pass http://user_service; }
    location /api/orders/ { proxy_pass http://order_service; }
    location / { proxy_pass http://monolith; }  # default: monolith
}
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/system-design