← Knowledge Base

System Architecture

A deep practical guide on when to use what — covering 27 components from monoliths to service meshes, with real trade-offs, diagrams, and decision frameworks.

1 Monolith Architecture

The default choice for most projects — and that's a good thing.

What It Is

A monolith is a single deployable unit containing all application logic. The entire codebase — UI, business logic, data access — is compiled and deployed together.

┌─────────────────────────────────────────────────────────┐ │ MONOLITH APP │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ UI / │ │ Business │ │ Data │ │ │ │ API │──│ Logic │──│ Access │──▶ Database │ │ │ Layer │ │ Layer │ │ Layer │ │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ ┌───────┐ ┌───────┐ ┌───────┐ ┌──────────┐ │ │ │Orders │ │Users │ │Payment│ │Inventory │ Modules │ │ └───────┘ └───────┘ └───────┘ └──────────┘ │ └─────────────────────────────────────────────────────────┘ │ Single Deploy

When to Use It

Use When Recommended

  • Your team is fewer than ~20 engineers
  • You're building a new product / MVP
  • Domain boundaries are unclear
  • You want simple local development and debugging
  • You need fast iteration speed
  • Deployment simplicity matters (one artifact, one pipeline)

Don't Use When Caution

  • Multiple teams need independent deployment cadences
  • Parts of the system need radically different scaling profiles (e.g., CPU-heavy image processing vs I/O-heavy API)
  • Regulatory/compliance requires strict service isolation
  • You genuinely have 100+ engineers on one codebase and build times are measured in hours

The Modular Monolith

The best of both worlds: keep a single deployable but enforce strict module boundaries internally. Each module owns its data, exposes an internal API, and can be extracted to a service later if needed.

┌────────────────────────────────────────────────────────┐ │ MODULAR MONOLITH │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │ │ Orders Module│ │ Users Module │ │Payment Module│ │ │ │ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌─────────┐ │ │ │ │ │ API │ │ │ │ API │ │ │ │ API │ │ │ │ │ │ Service │ │ │ │ Service │ │ │ │ Service │ │ │ │ │ │ Repo │ │ │ │ Repo │ │ │ │ Repo │ │ │ │ │ └──────────┘ │ │ └──────────┘ │ │ └─────────┘ │ │ │ │ Own Tables │ │ Own Tables │ │ Own Tables │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬──────┘ │ │ │ Module boundary (no cross-DB joins) │ │ └─────────┼──────────────────┼─────────────────┼─────────┘ ▼ ▼ ▼ ┌──────────────────────────────────────────┐ │ Shared Database │ │ orders_* │ users_* │ payment_* │ └──────────────────────────────────────────┘

Trade-offs

AdvantageDisadvantage
Simple deployment (1 artifact)Scaling is all-or-nothing
Easy local developmentLong build times as codebase grows
In-process calls (no network latency)Technology lock-in (one language/framework)
Simple transactions (ACID)Big-bang releases can be risky
Easy debugging/profilingOne bug can take down everything
"If you can't build a well-structured monolith, what makes you think microservices are the answer?" — Simon Brown

When NOT to Migrate to Microservices

⚠
Don't migrate because it's trendy. If your monolith is hard to maintain, the problem is usually poor code structure, not the architecture style. Fix the boundaries first — extract modules, enforce interfaces, split the database schema logically. Most teams that migrate to microservices prematurely end up with a distributed monolith — all the complexity of microservices with none of the benefits.

2 Microservices Architecture

Powerful but expensive — both in engineering effort and operational overhead.

What It Is

Independently deployable services, each owning its own data, communicating over the network (HTTP, gRPC, messaging). Each service is small enough to be owned by one team.

┌──────────┐ ┌────────▶│ Orders │──▶ Orders DB │ │ Service │ ┌────────┐ │ └──────────┘ │ API │───┤ │Gateway │ │ ┌──────────┐ └────────┘ ├────────▶│ Users │──▶ Users DB │ │ Service │ │ └──────────┘ │ │ ┌──────────┐ ┌──────────┐ └────────▶│ Payment │────────▶│ Stripe │ │ Service │──▶ Pay DB│ (ext) │ └──────────┘ └──────────┘ │ Message Bus ◀── Inventory Service ──▶ Inv DB

When It Genuinely Makes Sense

  1. Organizational alignment (Conway's Law) — You have multiple autonomous teams (6+ teams) that need to deploy independently without coordinating releases.
  2. Different scaling needs — Your search service needs 50 instances while your admin panel needs 2.
  3. Different technology requirements — ML model serving in Python, real-time API in Go, legacy integration in Java.
  4. Fault isolation — A failure in image processing must not take down checkout.
  5. You've already built a modular monolith — and you've identified the exact seams where services should be split.

The Costs Nobody Talks About

Network Latency

In-process function calls take nanoseconds. Network calls take milliseconds. A request that touches 5 services adds 5x network round trips. Multiply by retries and you have a latency problem.

Distributed Transactions

No more simple database transactions. You need sagas, compensating transactions, eventual consistency, and idempotency everywhere. This is genuinely hard to get right.

Operational Complexity

You need: service discovery, distributed tracing, centralized logging, health checks, circuit breakers, container orchestration, CI/CD per service, contract testing, API versioning.

Debugging

A bug that spans 3 services requires correlating logs across systems, understanding async message flows, and reasoning about eventual consistency states. Stack traces don't cross network boundaries.

Prerequisites Before Adopting

✅
You need these in place first: Automated CI/CD pipelines, container orchestration (Kubernetes or similar), centralized logging & monitoring, distributed tracing, service discovery, team-per-service ownership model, well-defined domain boundaries (via DDD or similar).

3 Service-Oriented Architecture (SOA)

The enterprise predecessor to microservices — still alive in large organizations.

What It Is

SOA organizes an application as a collection of services that communicate through an Enterprise Service Bus (ESB). Services tend to be larger than microservices and often share data through a common schema or canonical data model.

┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ CRM │ │ Billing │ │ Inventory │ │ Shipping │ │ Service │ │ Service │ │ Service │ │ Service │ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ ══════╧════════════════╧════════════════╧════════════════╧═════ Enterprise Service Bus (ESB) Routing │ Transformation │ Orchestration │ Protocol Mediation ══════════════════════════════════════════════════════════════

SOA vs Microservices

AspectSOAMicroservices
Service sizeLarger, business-domain scopedSmall, single-responsibility
CommunicationESB (smart pipes)Dumb pipes, smart endpoints
DataOften shared databaseDatabase per service
GovernanceCentralizedDecentralized
ProtocolSOAP/XML, WS-*REST/gRPC, JSON/Protobuf
DeploymentOften coordinatedIndependent

When Enterprises Still Use It

Large organizations with existing ESB investments (MuleSoft, IBM Integration Bus, TIBCO) continue using SOA because: (1) migration cost is enormous, (2) it works for their integration needs, (3) centralized governance suits regulated industries. New greenfield projects rarely choose SOA over microservices.

4 Event-Driven Architecture

React to things that happened rather than asking for things.

Core Concepts

Event Sourcing

Store every state change as an immutable event. Current state = replay all events. Gives you a complete audit log and the ability to rebuild state at any point in time.

Hard Increases storage, requires snapshots for performance, eventual consistency.

CQRS

Command Query Responsibility Segregation — separate the write model (commands) from the read model (queries). Write to a normalized store, project into denormalized read views.

When Read/write patterns differ drastically. Don't use for simple CRUD.

Event Bus

A central channel (Kafka, RabbitMQ, EventBridge) where producers emit events and consumers subscribe. Decouples producers from consumers completely.

Enables Loose coupling, independent scaling, async processing.

Producer A ──▶ ┌────────────────────┐ ──▶ Consumer X (Orders) │ │ Producer B ──▶ │ Event Bus │ ──▶ Consumer Y (Email) │ (Kafka / AMQP) │ Producer C ──▶ │ │ ──▶ Consumer Z (Analytics) └────────────────────┘ │ Events are immutable facts: "OrderPlaced", "PaymentReceived", "ItemShipped"

Eventual Consistency Trade-offs

Strong Consistency

Every read returns the latest write. Simple to reason about. Requires synchronous coordination. Limits availability (CAP theorem).

Use for Financial transactions, inventory counts, user auth.

vs

Eventual Consistency

Reads may return stale data temporarily. Higher availability and partition tolerance. Requires idempotent consumers and conflict resolution.

Use for Notifications, analytics, search indexes, recommendations.

When to Use Event-Driven

5 Serverless Architecture

No servers to manage — but plenty of trade-offs to understand.

Two Flavors

FaaS Functions as a Service

AWS Lambda, Google Cloud Functions, Azure Functions. Deploy individual functions triggered by events (HTTP, queue, schedule). Auto-scales to zero.

BaaS Backend as a Service

Firebase, Supabase, AWS Amplify. Managed services for auth, database, storage, push notifications. Write frontend code, let the platform handle the backend.

Client ──▶ API Gateway ──▶ Lambda (handler) ──▶ DynamoDB │ │ ├──▶ Lambda (auth) ├──▶ S3 │ │ └──▶ Lambda (resize-image) ◀── S3 Event Trigger Event Sources: HTTP, SQS, SNS, S3, DynamoDB Streams, CloudWatch Events

Trade-offs

FactorDetail
Cold StartsFirst invocation after idle: 100ms-5s depending on runtime (JVM worst, Node/Python best). Use provisioned concurrency for latency-sensitive paths — but that defeats the cost model.
Vendor Lock-inLambda + DynamoDB + API Gateway + Step Functions = deeply coupled to AWS. Mitigation: use frameworks like Serverless Framework or SST, keep business logic portable.
Cost ModelCheap for spiky/low traffic. Expensive for sustained high throughput. At ~1M requests/day consistently, a container is cheaper.
Execution LimitsMax execution time (15 min Lambda), memory limits, payload size limits, concurrency limits. Not suitable for long-running processes.
Local DevHard to replicate cloud triggers locally. Tools like SAM, LocalStack help but aren't perfect.
ObservabilityDistributed across many functions. Need structured logging and tracing (X-Ray, Datadog) from day one.

When Serverless Makes Sense

⚡
Best fits: Event-driven workflows (image processing, file uploads), webhooks, scheduled tasks (cron), low-traffic APIs, prototypes/MVPs, glue code between services. Avoid for: Latency-sensitive APIs, long-running processes, apps with steady high traffic, complex stateful workflows.

6 Synchronous Communication

REST, gRPC, GraphQL — and when each one wins.

Comparison

AspectRESTgRPCGraphQL
ProtocolHTTP/1.1 (or 2)HTTP/2HTTP (typically POST)
SerializationJSON (text)Protobuf (binary)JSON (text)
ContractOpenAPI / Swagger.proto filesSchema (SDL)
PerformanceGoodBest (binary, multiplexing)Varies (can over/under fetch)
StreamingSSE, WebSocket (separate)Native (bidirectional)Subscriptions (via WS)
Browser supportNativeNeeds grpc-web proxyNative
CachingHTTP caching built-inNo HTTP cachingComplex (needs persisted queries)
Learning curveLowMediumMedium-High
Best forPublic APIs, CRUD, webInternal service-to-serviceFrontend-driven APIs, mobile

When Each Wins

REST Wins When

  • Building public/external APIs
  • HTTP caching is important
  • Team is familiar with REST
  • Simple CRUD operations
  • Browser clients are primary

gRPC Wins When

  • Service-to-service communication
  • Low latency required
  • Bidirectional streaming needed
  • Strict typing via Protobuf
  • Polyglot services (code gen)

GraphQL Wins When

  • Multiple client types (mobile, web)
  • Clients need flexible queries
  • Avoiding over-fetching matters
  • Aggregating multiple backends
  • Rapid frontend iteration

API Versioning Strategies

StrategyExampleProsCons
URL path/api/v2/usersSimple, explicit, cacheableURL proliferation
HeaderAccept: application/vnd.api+json;v=2Clean URLsHard to test in browser
Query param/api/users?version=2Easy to switchCan pollute caching
No versioningAdditive changes onlySimplest long-termBreaking changes impossible

7 Asynchronous Communication

Decouple producers from consumers with message queues and event streams.

Message Queues vs Event Streams

Message Queue

Point-to-point. Producer sends a message, one consumer processes it and the message is removed. Think: task distribution.

Pattern: Work queue, request-reply.
Examples: RabbitMQ, SQS, ActiveMQ.

vs

Event Stream

Pub/sub with persistence. Events are appended to a log. Multiple consumers can read independently, replay from any offset. Think: event log.

Pattern: Event sourcing, CDC, fan-out.
Examples: Kafka, Redpanda, Pulsar.

Message Queue (Point-to-Point): ┌──────────┐ ┌─────────┐ ┌──────────┐ │ Producer │────▶│ Queue │────▶│ Consumer │ (message deleted after ack) └──────────┘ └─────────┘ └──────────┘ Event Stream (Pub/Sub with Log): ┌──────────┐ ┌─────────────────────────────────┐ │ Producer │────▶│ Topic [e1][e2][e3][e4][e5] │ └──────────┘ └─────────────────────────────────┘ │ │ Consumer A Consumer B (offset: 3) (offset: 1) ◀── independent progress

Communication Patterns

Fire-and-Forget

Producer sends and moves on. No response expected. Best for notifications, logging, analytics events.

Simple No confirmation

Request-Reply (Async)

Producer sends a message with a correlation ID and reply-to queue. Consumer processes and replies. Good for distributed RPC over messaging.

Decoupled Complex

Backpressure

When consumers can't keep up, the system must handle it: drop messages, buffer, slow down producers, or scale consumers. Kafka handles this via consumer lag; RabbitMQ via queue limits.

Critical at scale

8 Message Brokers

RabbitMQ vs Kafka vs SQS vs NATS — a detailed comparison.

FeatureRabbitMQKafkaAWS SQSNATS
ModelQueue + ExchangeDistributed logManaged queuePub/Sub + Queue
OrderingPer-queue FIFOPer-partitionFIFO (optional, extra cost)Per-subject (JetStream)
PersistenceOptional (durable queues)Always (configurable retention)4-14 daysJetStream (file/memory)
Throughput~50K msg/sMillions msg/s~3K msg/s (standard)~10M+ msg/s (core NATS)
DeliveryAt-least-once, at-most-onceAt-least-once, exactly-once (with transactions)At-least-onceAt-most-once (core), at-least-once (JS)
Consumer GroupsCompeting consumersNativeNo (use multiple queues)Queue groups
ReplayNo (message deleted)Yes (offset-based)NoYes (JetStream)
ProtocolAMQP, MQTT, STOMPCustom binaryHTTP (AWS SDK)Custom TCP
Ops ComplexityMediumHigh (ZK/KRaft, partitions)Zero (managed)Low
Best ForTask queues, RPC, routingEvent streaming, CDC, analyticsSimple cloud queues, decouplingLow-latency, IoT, edge

Pub/Sub vs Queue Pattern

Queue Pattern (competing consumers — work distribution): ┌──────┐ ┌──────────┐ ┌────────────┐ │Prod. │────▶│ Queue │────▶│ Consumer 1 │ (each msg goes to ONE consumer) └──────┘ │ │────▶│ Consumer 2 │ └──────────┘────▶│ Consumer 3 │ └────────────┘ Pub/Sub Pattern (fan-out — event notification): ┌──────┐ ┌──────────┐ ┌──── Subscriber A (Email service) │Prod. │────▶│ Topic / │────▶├──── Subscriber B (Analytics) └──────┘ │ Exchange │────▶└──── Subscriber C (Audit log) └──────────┘ (each msg goes to ALL subscribers)

When to Pick Which

Pick RabbitMQ when

  • You need complex routing (topic, header, fanout exchanges)
  • Task distribution with acknowledgments
  • Request-reply pattern over messaging
  • Message priority support needed
  • Moderate throughput is sufficient

Pick Kafka when

  • You need event replay / reprocessing
  • High throughput event streaming (100K+ events/s)
  • Change data capture (CDC) pipelines
  • Multiple consumers reading same events independently
  • Event sourcing / audit trail

Pick SQS when

  • You're on AWS and want zero ops overhead
  • Simple decoupling between services
  • Dead letter queue for failed messages
  • You don't need replay or consumer groups

Pick NATS when

  • Ultra-low latency required
  • Edge / IoT deployments
  • Simple pub/sub without persistence (core NATS)
  • Lightweight footprint (single binary, no JVM)

9 Service Mesh

Infrastructure-layer networking for microservices — Istio, Linkerd, and when it's overkill.

What It Solves

mTLS (Mutual TLS)

Automatic encryption and identity verification between all services. Zero-trust networking without changing application code.

Observability

Automatic distributed tracing, metrics (latency, error rates, throughput) for every service-to-service call. No instrumentation code needed.

Traffic Management

Canary deployments, A/B testing, traffic shifting, header-based routing, fault injection for chaos testing.

Resilience

Retries, timeouts, circuit breakers, rate limiting — all configured via policies, not code.

Sidecar Proxy Pattern

┌──────────────────────────────────────────────────────────────────┐ │ DATA PLANE │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ Pod A │ │ Pod B │ │ │ │ ┌───────┐ ┌───────┐ │ mTLS │ ┌───────┐ ┌───────┐│ │ │ │ │Service│◀▶│Envoy │◀╌╌╌╌╌╌╌╌▶│ │Envoy │◀▶│Service││ │ │ │ │ A │ │Sidecar│ │ │ │Sidecar│ │ B ││ │ │ │ └───────┘ └───────┘ │ │ └───────┘ └───────┘│ │ │ └─────────────────────┘ └─────────────────────┘ │ └──────────────────────────────────────────────────────────────────┘ ▲ │ Configuration ┌────────────────────────────┴─────────────────────────────────────┐ │ CONTROL PLANE │ │ ┌─────────┐ ┌──────────┐ ┌────────────┐ │ │ │ Pilot │ │ Citadel │ │ Galley │ (Istio) │ │ │(routing)│ │ (certs) │ │ (config) │ │ │ └─────────┘ └──────────┘ └────────────┘ │ └──────────────────────────────────────────────────────────────────┘

Istio vs Linkerd

AspectIstioLinkerd
ProxyEnvoy (C++)linkerd2-proxy (Rust)
ComplexityHigh (many CRDs, config options)Low (opinionated, fewer knobs)
Resource usageHigherLower
FeaturesMost comprehensiveCore features, simpler
CommunityLarge (Google, IBM)Smaller (CNCF graduated)

When It's Overkill

⚠
You probably don't need a service mesh if: You have fewer than 10 services, your team is small, you can handle mTLS at the infrastructure level (e.g., AWS App Mesh, VPC), you don't need advanced traffic management. The complexity cost of a service mesh (debugging proxy issues, resource overhead of sidecars, learning curve) is significant. Start simple, add it when you hit real pain.

10 API Gateway

The front door to your backend services.

What It Does

Client Request
➔
Auth / JWT Validation
➔
Rate Limiting
➔
Request Transform
➔
Route to Service
➔
Response Transform
┌──────────────────────────────────┐ │ API GATEWAY │ ┌────────┐ │ ┌─────────────────────────────┐ │ ┌──────────┐ │ Mobile │───────▶│ │ • Authentication (JWT/OAuth) │ │───────▶│ Users │ │ App │ │ │ • Rate Limiting │ │ │ Service │ └────────┘ │ │ • Request Routing │ │ └──────────┘ │ │ • Load Balancing │ │ ┌────────┐ │ │ • SSL Termination │ │ ┌──────────┐ │ Web │───────▶│ │ • Caching │ │───────▶│ Orders │ │ App │ │ │ • Request/Response Transform │ │ │ Service │ └────────┘ │ │ • API Versioning │ │ └──────────┘ │ │ • Logging / Metrics │ │ ┌────────┐ │ └─────────────────────────────┘ │ ┌──────────┐ │3rd Party│──────▶│ │───────▶│ Payment │ └────────┘ └──────────────────────────────────┘ │ Service │ └──────────┘

Comparison

GatewayTypeStrengthsWeaknesses
KongSelf-hosted / CloudPlugin ecosystem, Lua extensibility, PostgreSQL/Cassandra backingComplex to operate at scale, Enterprise features paywalled
AWS API GWManaged (AWS)Zero ops, Lambda integration, WebSocket supportVendor lock-in, cold starts, limited customization, per-request pricing
NginxSelf-hostedBattle-tested, fast, huge communityConfig file-driven (less dynamic), limited API-specific features without Plus
TraefikSelf-hostedAuto-discovery (Docker, K8s labels), Let's Encrypt built-in, GoLess mature plugin ecosystem than Kong
EnvoySelf-hostedgRPC-native, xDS dynamic config, foundation for IstioSteep learning curve, YAML-heavy

When You Need One vs Simple Reverse Proxy

🔎
Use a full API Gateway when: You have multiple backend services, need auth at the edge, rate limiting, API key management, request transformation, or developer portal.
A simple reverse proxy (Nginx/Caddy) suffices when: Single backend, basic routing, SSL termination is all you need.

11 Reverse Proxy vs Forward Proxy vs Load Balancer

Three related but distinct concepts.

Forward Proxy (client-side): ┌────────┐ ┌─────────────┐ ┌──────────┐ │ Client │────▶│Forward Proxy│────▶│ Server │ └────────┘ └─────────────┘ └──────────┘ Client knows about proxy. Proxy hides client identity. Use: corporate firewalls, anonymity, content filtering. Reverse Proxy (server-side): ┌────────┐ ┌──────────────┐ ┌──────────┐ │ Client │────▶│Reverse Proxy │────▶│ Server │ └────────┘ └──────────────┘ └──────────┘ Client doesn't know about backend. Proxy hides server identity. Use: SSL termination, caching, compression, security. Load Balancer: ┌────────┐ ┌──────────────┐ ┌──────────┐ │ Client │────▶│Load Balancer │──┬─▶│ Server 1 │ └────────┘ └──────────────┘ ├─▶│ Server 2 │ └─▶│ Server 3 │ Distributes traffic across multiple backends. Algorithms: Round-robin, Least connections, IP hash, Weighted.

Tool Comparison

ToolPrimary RoleL4/L7Best For
NginxReverse proxy + LBBothWeb serving, reverse proxy, simple LB. Most popular.
HAProxyLoad balancerBoth (excels L4)High-performance TCP/HTTP load balancing, health checks.
EnvoyService proxyL7Service mesh sidecar, gRPC, dynamic configuration, advanced observability.
CaddyReverse proxyL7Auto HTTPS (Let's Encrypt), simple config, small projects.
AWS ALB/NLBManaged LBALB=L7, NLB=L4AWS-native, zero ops, auto-scaling.

12 Backend for Frontend (BFF)

Tailored backends for each client type.

┌──────────────┐ ┌────────────┐ ┌────────▶│ Web BFF │──┬──▶ Users Service │ Web App │────┘ │ (optimized │ ├──▶ Orders Service └────────────┘ │ for web) │ └──▶ Search Service └──────────────┘ ┌──────────────┐ ┌────────────┐ ┌────────▶│ Mobile BFF │──┬──▶ Users Service │ Mobile App │────┘ │ (smaller │ └──▶ Orders Service └────────────┘ │ payloads) │ └──────────────┘ ┌──────────────┐ ┌────────────┐ ┌────────▶│3rd Party BFF │──┬──▶ Public API Service │ Partners │────┘ │(rate limited, │ └──▶ Auth Service └────────────┘ │ versioned) │ └──────────────┘

When to Use BFF

BFF vs Single API Gateway

BFF Pattern

One backend per client type. Each BFF aggregates, transforms, and optimizes for its client. More code to maintain, but maximum flexibility.

vs

Single API Gateway

One gateway for all clients. Simpler to operate, but all clients get the same API shape. Use field selection or GraphQL to let clients choose what they need.

13 GraphQL Federation

Compose multiple GraphQL services into one unified graph.

┌───────────────────┐ Client ───────────────▶│ Gateway / Router │ │ (Apollo Router) │ └─────────┬─────────┘ Query planning │ & execution ┌──────────────────┼──────────────────┐ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ Users │ │ Products │ │ Reviews │ │ Subgraph │ │ Subgraph │ │ Subgraph │ └────────────┘ └────────────┘ └────────────┘ │ │ │ Users DB Products DB Reviews DB query { user(id: "1") { ◀── resolved by Users subgraph name orders { ◀── resolved by Orders subgraph product { ◀── resolved by Products subgraph name reviews { ... } ◀── resolved by Reviews subgraph } } } }

When Federation Makes Sense

Good Fit

  • Multiple teams own different parts of the graph
  • Frontend needs to query across domains in one request
  • You already have GraphQL and want to split a monolith graph
  • Schema governance is important

Avoid When

  • You have fewer than 3 services (use a single GraphQL server)
  • Team doesn't have GraphQL expertise
  • REST aggregation with a BFF would be simpler
  • Performance is critical (federation adds latency for cross-subgraph queries)

14 Caching

The most impactful performance optimization — and the hardest to get right.

Cache Layers

┌────────┐ ┌──────────┐ ┌───────────┐ ┌──────────────┐ ┌───────────┐ ┌──────────┐ │ Client │────▶│ Browser │────▶│ CDN │────▶│ API Gateway │────▶│App Server │────▶│ Database │ │ │ │ Cache │ │ (Edge) │ │ Cache │ │ Cache │ │ Cache │ └────────┘ └──────────┘ └───────────┘ └──────────────┘ └───────────┘ └──────────┘ HTTP cache CloudFront, Response cache Redis / Query cache, headers Fastly, (short TTL) Memcached, buffer pool (ETag, max-age) Cloudflare local (Caffeine)

Caching Strategies

Cache-Aside (Lazy Loading) Most Common

App checks cache first. On miss, reads from DB and writes to cache. App manages both reads and writes. Simple, good for read-heavy workloads.

Risk: Cache and DB can drift. Use TTL to bound staleness.

Write-Through

Every write goes to cache AND database synchronously. Cache is always up-to-date. Higher write latency (two writes per operation).

Best for: Data that's read immediately after writing.

Write-Behind (Write-Back)

Write to cache immediately, asynchronously flush to database. Very fast writes. Risk of data loss if cache node fails before flush.

Best for: Write-heavy workloads where some data loss is acceptable.

Read-Through

Cache itself is responsible for loading from DB on miss (cache acts as the data source). Simplifies app code but requires cache library support.

Best for: When you want the cache layer to handle all data loading logic.

Redis vs Memcached vs Local Cache

FeatureRedisMemcachedLocal (Caffeine/Guava)
Data structuresRich (strings, hashes, lists, sets, sorted sets, streams)Strings onlyKey-value
PersistenceRDB + AOFNoneIn-process (lost on restart)
ClusteringRedis ClusterClient-side shardingN/A (per-instance)
Pub/SubYesNoNo
Latency~0.5ms (network)~0.5ms (network)~ns (in-process)
Use whenShared cache, sessions, leaderboards, rate limitingSimple distributed cache, large blob storageHot data, per-instance cache, no consistency needs

Cache Invalidation

"There are only two hard things in Computer Science: cache invalidation and naming things." — Phil Karlton

Strategies: TTL-based (set expiry, accept staleness window), Event-based (invalidate on write via pub/sub), Version-based (cache key includes version/hash). In practice, use TTL as a safety net combined with event-based invalidation for critical data.

When Caching Hurts

⚠
Caching is not free. Stale data bugs are hard to diagnose. Cache stampede (thundering herd) on expiry can overload the DB. Over-caching masks underlying performance problems. Don't cache data that changes frequently and must be consistent. Don't cache if the dataset is small enough to fit in the DB buffer pool.

15 Database Selection Guide

Choosing the right database for the job — a comprehensive comparison.

TypeDatabaseData ModelBest Use CasesConsistencyScaling
RelationalPostgreSQLTables, rows, SQLGeneral purpose, complex queries, JSONB support, GISStrong (ACID)Vertical + read replicas
RelationalMySQLTables, rows, SQLWeb apps, WordPress, mature ecosystemStrong (ACID)Vertical + read replicas
DocumentMongoDBJSON documentsFlexible schemas, content management, catalogs, prototypingTunableHorizontal (sharding)
Key-ValueRedisKey → valueCaching, sessions, leaderboards, rate limiting, pub/subSingle-key atomicRedis Cluster
Key-ValueDynamoDBKey → value/docServerless, known access patterns, high throughputEventual/StrongHorizontal (managed)
Wide-ColumnCassandraColumn familiesTime-series at scale, IoT, write-heavy, multi-DCTunable (eventual default)Horizontal (linear)
GraphNeo4jNodes + edgesSocial networks, recommendations, fraud detection, knowledge graphsACIDVertical (cluster for HA)
Time-SeriesInfluxDB / TimescaleDBTime-indexed pointsMetrics, monitoring, IoT telemetry, financial ticksVariesHorizontal (TimescaleDB on PG)
SearchElasticsearchInverted indexFull-text search, log analytics, autocomplete, geo searchNear real-timeHorizontal (sharding)

Decision Diagram

What's your primary need? │ ├─ Structured data with relationships? ──▶ PostgreSQL (default choice) │ └─ Need graph traversals (friends-of-friends)? ──▶ Neo4j │ ├─ Flexible / nested documents? ──▶ MongoDB │ └─ On AWS serverless? ──▶ DynamoDB │ ├─ High-speed caching / sessions? ──▶ Redis │ ├─ Write-heavy time-series data? ──▶ TimescaleDB (if you know SQL) │ InfluxDB (purpose-built) │ Cassandra (massive scale) │ ├─ Full-text search? ──▶ Elasticsearch │ └─ Simple search on PostgreSQL? ──▶ pg_trgm + GIN index │ └─ Massive scale, simple access patterns? ──▶ DynamoDB / Cassandra
💡
Default to PostgreSQL. PostgreSQL handles 90% of use cases well: relational data, JSON (JSONB), full-text search (good enough for most), time-series (TimescaleDB extension), and even graph queries (recursive CTEs). Only reach for a specialized database when PostgreSQL demonstrably can't meet your requirements.

16 Database Patterns

Patterns for scaling, consistency, and distributed data management.

Read Replicas

┌────────────┐ writes ┌──────────────┐ async replication ┌─────────────┐ │ App Server │──────────▶│ Primary │─────────────────────▶│ Replica 1 │ │ │ │ (read/write) │ └─────────────┘ │ │ reads └──────────────┘─────────────────────▶┌─────────────┐ │ │─────────────────────────────────────────────────▶│ Replica 2 │ └────────────┘ └─────────────┘ Use when: Read-heavy workloads (90% reads). Replication lag = eventual consistency for reads.

Connection Pooling

Database connections are expensive (TCP handshake, TLS, authentication). Connection pools maintain a set of reusable connections.

PoolerLevelBest For
HikariCPApplication (JVM)Java/Kotlin apps. Fastest JVM pool. Default in Spring Boot.
PgBouncerExternal proxyPostgreSQL. Sits between app and DB. Essential for serverless (Lambda + PG).
ProxySQLExternal proxyMySQL. Read/write splitting, query caching, connection multiplexing.

Database per Service vs Shared Database

Database per Service

Each microservice owns its database. Full autonomy, independent schema evolution, technology freedom. But: no cross-service joins, distributed transactions needed (sagas).

Microservices ideal

vs

Shared Database

Multiple services read/write the same database. Simpler joins and transactions. But: tight coupling, schema changes affect all services, scaling bottleneck.

Monolith / early stage

Saga Pattern for Distributed Transactions

Choreography Saga (event-driven): Order Service ──[OrderCreated]──▶ Payment Service ──[PaymentProcessed]──▶ Inventory Service ▲ │ └───[PaymentFailed → Compensate]─────┘ Orchestration Saga (central coordinator): ┌──────────────────┐ │ Saga │ 1. Create Order ──▶ Order Service │ Orchestrator │ 2. Process Payment ─▶ Payment Service │ │ 3. Reserve Stock ──▶ Inventory Service │ (manages state, │ 4. If any fails: ──▶ Run compensating actions │ retries, │ │ compensation) │ └──────────────────┘

Choreography: simpler, but harder to understand the full flow as it's distributed across services. Orchestration: centralized logic, easier to reason about, but the orchestrator is a single point to maintain.

17 Object Storage & File Storage

S3, block storage, NFS — when to use what.

TypeExamplesAccess PatternBest For
Object StorageS3, MinIO, GCS, Azure BlobHTTP API (PUT/GET), flat namespace, immutable objectsImages, videos, backups, data lake, static assets. Infinite scale, cheap.
Block StorageEBS, Azure Disk, Persistent VolumesMounted as disk, random read/writeDatabase volumes, OS disks, any app that needs a filesystem. Fixed size, fast I/O.
File Storage (NFS)EFS, FSx, NFS serversPOSIX filesystem, shared mountShared filesystem across multiple servers, legacy apps, CMS uploads. Higher latency than block.

CDN Integration Pattern

┌────────┐ ┌───────────────┐ ┌──────────────────┐ │ Client │─────▶│ CDN Edge │─────▶│ S3 / MinIO │ │ │ │ (CloudFront, │ │ (Origin) │ │ │◀─────│ Fastly) │◀─────│ │ └────────┘ └───────────────┘ └──────────────────┘ Cached at edge Upload via presigned URL (TTL-based) (avoids passing through your server) Upload flow: 1. Client requests presigned URL from your API 2. API generates presigned S3 URL (expires in 5 min) 3. Client uploads directly to S3 (no server bandwidth used) 4. S3 event triggers processing (thumbnail, virus scan, etc.)

18 Circuit Breaker Pattern

Stop cascading failures before they take down your entire system.

State Machine

failure threshold exceeded ┌──────────────────────────────────────────┐ │ ▼ ┌─────────┐ ┌──────────┐ │ CLOSED │ │ OPEN │ │(normal) │ │(failing) │ │ │ │ reject │ │ count │ │ all calls│ │failures │ │ │ └─────────┘ └────┬─────┘ ▲ │ │ success timeout │ │ threshold met expires │ │ ▼ │ ┌───────────────┐ └──────────────────────────────│ HALF-OPEN │ │ (testing) │ │ allow limited │ │ requests │ └───────────────┘ │ │ failure → back to OPEN ▼

How It Works

  1. CLOSED: Requests flow normally. Failures are counted. If failure rate exceeds threshold (e.g., 50% of last 100 calls), circuit opens.
  2. OPEN: All requests are immediately rejected (fail-fast) with a fallback response. No calls to the downstream service. Timer starts.
  3. HALF-OPEN: After timeout, a limited number of test requests are allowed through. If they succeed, circuit closes. If they fail, circuit reopens.

Implementation

LibraryLanguageStatus
Resilience4jJava/KotlinActive — recommended for JVM
HystrixJavaMaintenance mode — Netflix deprecated it
Polly.NETActive
opossumNode.jsActive
Service meshAnyIstio/Linkerd provide circuit breaking at the proxy level (no library needed)

When to Use

19 Retry & Timeout Patterns

Handling transient failures gracefully without making things worse.

Exponential Backoff with Jitter

Attempt 1: immediate Attempt 2: wait 1s + random(0, 500ms) Attempt 3: wait 2s + random(0, 1000ms) Attempt 4: wait 4s + random(0, 2000ms) Attempt 5: wait 8s + random(0, 4000ms) ← cap at max backoff Give up. Why jitter? Without it, all retrying clients retry at the same time (thundering herd), overloading the recovering service.

Key Patterns

Idempotency Critical

If a request might be retried, the operation MUST be idempotent (same result if called multiple times). Use idempotency keys: client sends a unique key, server deduplicates. Essential for payment operations.

Timeout Cascades

Service A calls B calls C. If C is slow, B waits, A waits. Set timeouts at each level, with outer timeouts greater than inner ones. Example: A timeout = 5s, B timeout to C = 3s. This prevents thread pool exhaustion.

Bulkhead Pattern

Isolate different call types into separate thread pools or connection pools. A slow dependency in pool A doesn't exhaust resources for pool B. Like ship bulkheads — one leak doesn't sink the ship.

Retry Budget

Limit total retries across the system. "Retry at most 10% of original requests." Prevents retry storms where retries generate more load than original traffic, worsening an outage.

Timeout Guidelines

⏰
Always set timeouts. Always. No timeout = potential thread leak. Default timeouts in HTTP clients are often 30-60 seconds — way too long. Set aggressive timeouts (e.g., 1-3 seconds for inter-service calls) and let circuit breakers handle the rest. A fast failure is better than a slow failure.

20 Rate Limiting

Protect your services from abuse and overload.

Algorithms Compared

AlgorithmHow It WorksProsCons
Token BucketBucket fills with tokens at a fixed rate. Each request consumes a token. Allows bursts up to bucket size.Allows bursts, simple, memory efficientBurst can overload downstream
Leaky BucketRequests enter a queue that drains at a fixed rate. Overflow is rejected.Smooth output rate, no burstsNo burst tolerance, can cause queuing delay
Fixed WindowCount requests in fixed time windows (e.g., per minute). Reset at window boundary.Simple to implementBoundary problem: 2x burst at window edges
Sliding Window LogTrack timestamp of each request. Count requests in the sliding window.Accurate, no boundary problemMemory heavy (stores every timestamp)
Sliding Window CounterHybrid: use previous + current window counts with weighted average.Accurate, memory efficientSlightly approximate

Where to Implement

┌────────┐ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐ │ Client │────▶│ API Gateway │────▶│ Per-Service │────▶│ Database │ │ │ │ Rate Limit │ │ Rate Limit │ │ Connection │ └────────┘ │ (global) │ │ (fine-grained)│ │ Limits │ └──────────────┘ └───────────────┘ └──────────────┘ Layer 1: Coarse Layer 2: Business Layer 3: Infra (IP, API key) (user, endpoint) (connection pool) Distributed rate limiting: Use Redis (INCR + EXPIRE) or a dedicated service. Each instance checks the shared counter.

Distributed Rate Limiting with Redis

🔧
Pattern: Use Redis INCR + EXPIRE (or Lua script for atomicity) with the key pattern rate:{user_id}:{window}. Each instance atomically increments the counter. When it exceeds the limit, return HTTP 429 with Retry-After header. Use sliding window counter for accuracy.

21 Logging, Metrics, Tracing

The three pillars of observability — you need all three.

┌─────────────────────────────────────────────────────────────┐ │ THE THREE PILLARS │ │ │ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ │ LOGS │ │ METRICS │ │ TRACES │ │ │ │ │ │ │ │ │ │ │ │ What │ │ How much │ │ Where │ │ │ │ happened │ │ / how fast│ │ time was │ │ │ │ (events) │ │ (numbers) │ │ spent │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ ▼ ▼ ▼ │ │ ELK / Loki / Prometheus / Jaeger / Zipkin / │ │ CloudWatch Datadog OpenTelemetry │ │ ▼ ▼ ▼ │ │ └─────────── Grafana ──────────────┘ │ │ (unified dashboards) │ └─────────────────────────────────────────────────────────────┘

Stack Comparison

ComponentOption AOption BManaged Alternative
LoggingELK (Elasticsearch + Logstash + Kibana)Grafana Loki + PromtailDatadog, CloudWatch, Splunk
MetricsPrometheus + GrafanaInfluxDB + GrafanaDatadog, New Relic, CloudWatch
TracingJaegerZipkinDatadog APM, AWS X-Ray, Honeycomb
UnifiedOpenTelemetry (vendor-neutral SDK for logs, metrics, traces — the emerging standard)

Best Practices

Structured Logging

Log as JSON, not free text. Include: timestamp, level, service, correlation_id, user_id, message. Makes logs searchable and parseable.

Correlation IDs

Generate a unique ID at the entry point (API Gateway). Pass it through every service call (header: X-Correlation-ID). Include it in every log line. Lets you trace a request across all services.

What to Monitor

The RED method: Rate (requests/sec), Errors (error rate), Duration (latency percentiles p50, p95, p99). Plus: saturation (CPU, memory, disk, connections).

22 Health Checks & Alerting

Know when things break before your users do.

Kubernetes Probes

ProbePurposeOn FailureExample
LivenessIs the process alive and not deadlocked?Container is restartedGET /health/live → 200 (always, unless stuck)
ReadinessCan this instance handle traffic?Removed from load balancer (not restarted)GET /health/ready → checks DB connection, cache, dependencies
StartupHas the app finished initializing?Other probes disabled until this passesFor slow-starting apps (JVM warmup, data loading)

SLOs / SLIs / SLAs

SLI Indicator

A measurable metric. Example: "99.2% of requests completed in under 300ms over the last 28 days." The actual measurement.

SLO Objective

Internal target. "We aim for 99.9% availability." Triggers alerts and engineering priorities when at risk. More stringent than the SLA.

SLA Agreement

External contract with customers. "We guarantee 99.5% uptime or credits are issued." Has financial/legal consequences.

Alerting Strategy

🔔
Alert on symptoms, not causes. Alert on: error rate spike, latency p99 exceeds SLO, error budget burn rate too high. Don't alert on: CPU at 80% (unless it's causing symptoms), single failed health check (use sustained failure). Every alert should be actionable — if the on-call engineer can't do anything about it at 3 AM, it shouldn't page them.

23 Containers & Orchestration

Docker, Kubernetes — and when K8s is overkill.

Kubernetes Core Concepts

┌──────────────────── Kubernetes Cluster ────────────────────────┐ │ │ │ ┌─────────────────── Namespace: production ──────────────┐ │ │ │ │ │ │ │ Deployment (manages ReplicaSet) │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ ReplicaSet (ensures N pods running) │ │ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ │ │ Pod 1 │ │ Pod 2 │ │ Pod 3 │ │ │ │ │ │ │ │┌───────┐│ │┌───────┐│ │┌───────┐│ │ │ │ │ │ │ ││ App ││ ││ App ││ ││ App ││ │ │ │ │ │ │ │└───────┘│ │└───────┘│ │└───────┘│ │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Service (stable network endpoint → pods) │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ ClusterIP / NodePort / LoadBalancer │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ Ingress (external HTTP routing → services) │ │ │ │ ┌──────────────────────────────────────────────────┐ │ │ │ │ │ nginx-ingress / traefik / ALB controller │ │ │ │ │ │ host/path routing, TLS termination │ │ │ │ │ └──────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────┘ │ └────────────────────────────────────────────────────────────────┘

When Kubernetes Is Overkill

K8s Makes Sense When

  • 10+ microservices that need orchestration
  • Need auto-scaling, self-healing, rolling updates
  • Multi-team, multi-service deployments
  • You have (or can hire) Kubernetes expertise
  • Running on multiple clouds or on-premises

K8s Is Overkill When

  • 1-3 services (use Docker Compose or ECS)
  • Small team without K8s experience
  • Serverless/PaaS would be simpler (Heroku, Fly.io, Railway)
  • You're managing more YAML than application code

Alternatives

ToolComplexityBest For
Docker ComposeLowLocal dev, simple multi-container apps, small production deployments
AWS ECSMediumAWS-native container orchestration without K8s complexity
HashiCorp NomadMediumSimpler orchestration, supports containers + non-containers, easy to operate
Docker SwarmLowSimple clustering, built into Docker. Limited features compared to K8s.
Fly.io / RailwayVery LowDeploy containers with zero orchestration setup. PaaS simplicity.

24 CI/CD Pipeline

Build, test, deploy — reliably and repeatedly.

Pipeline Stages

Commit
➔
Build
➔
Unit Tests
➔
Integration Tests
➔
Security Scan
➔
Build Image
➔
Deploy Staging
➔
E2E Tests
➔
Deploy Prod

Deployment Strategies

Blue-Green

Run two identical environments. Deploy to the idle one (green), test it, then switch traffic. Instant rollback by switching back.

Fast rollback 2x resources

Canary

Route a small percentage (1-5%) of traffic to the new version. Monitor error rates and latency. Gradually increase if healthy. Roll back if issues detected.

Low risk Needs good observability

Rolling

Replace instances one at a time. Default in Kubernetes. No extra resources needed. Slower rollback (must roll forward or replace all).

Resource efficient Mixed versions during deploy

Feature Flags

Decouple deployment from release. Deploy code to production but keep it behind a flag. Enable for specific users (beta testers), percentages, or all users. Tools: LaunchDarkly, Unleash, Flagsmith, or simple config.

GitOps (ArgoCD, Flux)

Developer ──push──▶ Git Repo ◀──watches── ArgoCD/Flux ──syncs──▶ Kubernetes (source of truth) (controller) (actual state) Principle: The desired state of your infrastructure is defined in Git. The GitOps controller continuously reconciles actual state with desired state. All changes go through Git (PR review, audit trail, rollback = git revert).

25 Infrastructure as Code

Define infrastructure in version-controlled files, not ClickOps.

ToolLanguageCloud SupportStateBest For
TerraformHCL (declarative)Multi-cloudRemote state file (S3, Terraform Cloud)Most teams, multi-cloud, largest ecosystem of providers
PulumiTypeScript, Python, Go, etc.Multi-cloudPulumi Cloud or self-managedTeams that prefer real programming languages over DSLs
CloudFormationJSON/YAML (declarative)AWS onlyManaged by AWSAWS-only shops, tight AWS integration, no state management
CDKTypeScript, Python, etc.AWS (synths to CFN)Managed by AWSAWS users who want programming languages + CloudFormation
OpenTofuHCLMulti-cloudSame as TerraformOpen-source Terraform fork (BSL license concerns)

Drift Management

🔄
Drift = actual infrastructure differs from code. Causes: manual changes in console, another team modifying resources, failed applies. Detect: terraform plan (shows diff), scheduled drift detection. Prevent: lock down console access (read-only for most), require all changes through IaC pipeline, use policy-as-code (OPA, Sentinel) to enforce compliance.

26 Architecture Decision Records

Document the "why" behind decisions so future you isn't confused.

What Is an ADR?

A short document capturing an architectural decision: the context, the options considered, the decision made, and the consequences. Stored in version control alongside the code (typically docs/adr/).

Template

ADR-001: Use PostgreSQL as primary database

Status: Accepted
Date: 2025-01-15

Context:
We need a primary database for the application. Our data is highly relational (users, orders, products with many relationships). We need ACID transactions for payment processing. Team has strong SQL experience.

Decision:
We will use PostgreSQL as our primary database.

Alternatives Considered:
- MySQL: Similar capabilities, but PostgreSQL has better JSON support, better extension ecosystem (PostGIS, TimescaleDB).
- MongoDB: Flexible schema, but our data is relational. Would need to denormalize and lose referential integrity.

Consequences:
- (+) Strong consistency, ACID transactions
- (+) JSONB for semi-structured data when needed
- (+) Team expertise, large community
- (-) Vertical scaling primarily (read replicas for reads)
- (-) Need to manage schema migrations carefully

Tips

27 When to Use What — Decision Matrix

Quick-reference decision trees for common architectural choices.

I Need Caching

Is the data read frequently and rarely changes? │ ├─ YES → Is consistency critical? │ ├─ YES → Cache-aside + short TTL + event-based invalidation → Redis │ └─ NO → Cache-aside + longer TTL → Redis or CDN (for static) │ ├─ Is it per-user session data? → Redis (with expiry) │ ├─ Is it static content (images, CSS, JS)? → CDN (CloudFront, Fastly) │ ├─ Is it hot data accessed within one service instance? │ → Local cache (Caffeine/Guava) — fastest, no network hop │ ├─ Do you need shared cache across instances? │ → Redis (rich data structures, persistence) or Memcached (simple k/v, multi-threaded) │ └─ Don't cache if: data changes on every request, dataset fits in DB buffer pool, or consistency is non-negotiable.

I Need Async Communication

What's the use case? │ ├─ Task distribution (process jobs in background)? │ ├─ Simple, on AWS? → SQS │ ├─ Need routing, priorities, RPC? → RabbitMQ │ └─ Need exactly-once? → SQS FIFO or Kafka with transactions │ ├─ Event streaming (log of events, multiple consumers)? │ ├─ Need replay / reprocessing? → Kafka or Redpanda │ ├─ Need consumer groups? → Kafka │ └─ Simple fan-out? → SNS (AWS) or RabbitMQ fanout exchange │ ├─ Low-latency pub/sub (IoT, edge)? │ → NATS │ └─ Fire-and-forget notifications? → SNS, or RabbitMQ with no-ack

I Need a Database

What's your data like? │ ├─ Structured, relational, need joins & transactions? │ → PostgreSQL (default) or MySQL │ ├─ Semi-structured documents, flexible schema? │ ├─ On AWS serverless? → DynamoDB │ └─ General purpose? → MongoDB (or PostgreSQL JSONB if schema is mostly stable) │ ├─ Key-value lookups at extreme speed? │ → Redis (in-memory) or DynamoDB (managed, persistent) │ ├─ Time-series data (metrics, IoT, logs)? │ ├─ Want SQL? → TimescaleDB (PostgreSQL extension) │ └─ Purpose-built? → InfluxDB │ ├─ Full-text search? │ ├─ Advanced (facets, fuzzy, geo)? → Elasticsearch │ └─ Good enough? → PostgreSQL full-text search (pg_trgm + GIN) │ ├─ Graph relationships (social, recommendations, fraud)? │ → Neo4j │ └─ Write-heavy, massive scale, multi-datacenter? → Cassandra or ScyllaDB

I Need an API Style

Who's the consumer? │ ├─ Public / external developers? │ → REST (universally understood, HTTP caching, OpenAPI docs) │ ├─ Internal service-to-service? │ ├─ Need streaming? → gRPC │ ├─ Need maximum performance? → gRPC (binary, HTTP/2) │ └─ Team prefers simplicity? → REST (JSON) │ ├─ Multiple frontends needing different data shapes? │ ├─ Can adopt GraphQL? → GraphQL (flexible queries, one endpoint) │ └─ Prefer REST? → BFF pattern (one backend per client type) │ └─ Real-time bidirectional? → WebSockets or gRPC bidirectional streaming

I Need an API Gateway

How many backend services? │ ├─ One service? → Reverse proxy (Nginx, Caddy) is sufficient │ ├─ Multiple services? │ ├─ Need auth, rate limiting, API keys, developer portal? │ │ ├─ On AWS? → AWS API Gateway (managed, pay-per-use) │ │ ├─ Need extensibility / plugins? → Kong │ │ └─ On Kubernetes? → Traefik or Envoy-based (Ambassador, Emissary) │ │ │ └─ Just routing + SSL? │ → Nginx or Caddy (simple reverse proxy config) │ └─ Microservices with service mesh? → Envoy (via Istio) handles routing; API GW at edge only

Architecture Style Selection

What's your situation? │ ├─ New project, small team (<15 engineers)? │ → Monolith (modular). Don't overthink it. │ ├─ Growing product, clear domain boundaries, 3+ teams? │ → Extract services from the monolith at proven seams. │ Start with 2-3 services, not 20. │ ├─ Large org, 50+ engineers, multiple products? │ → Microservices with clear ownership. Invest in platform team. │ ├─ Event-heavy (IoT, real-time, analytics)? │ → Event-driven architecture (Kafka, event sourcing) │ ├─ Spiky / unpredictable traffic, event-triggered workloads? │ → Serverless (Lambda) for those specific functions │ └─ Large enterprise, existing ESB, complex integrations? → SOA may still be the pragmatic choice

Comprehensive Quick Reference

I Need...Default ChoiceConsider Instead If...
A databasePostgreSQLKey-value speed (Redis), documents (MongoDB), graph (Neo4j), time-series (TimescaleDB)
CachingRedisIn-process only (Caffeine), simple blob cache (Memcached), static assets (CDN)
Message brokerSQS (AWS) / RabbitMQ (self-hosted)Event streaming (Kafka), ultra-low-latency (NATS)
API styleRESTInternal + performance (gRPC), flexible frontend queries (GraphQL)
Container orchestrationDocker Compose (small) / ECS (AWS)Multi-cloud / complex (Kubernetes), simpler alt (Nomad)
IaCTerraformAWS-only (CDK/CloudFormation), real languages (Pulumi)
CI/CDGitHub ActionsComplex pipelines (GitLab CI), enterprise (Jenkins), GitOps (ArgoCD)
ObservabilityOpenTelemetry + Grafana stackManaged (Datadog), AWS-native (CloudWatch + X-Ray)
API GatewayNginx (simple) / Kong (advanced)AWS (API Gateway), K8s-native (Traefik), service mesh (Envoy)
Deployment strategyRolling (default in K8s)Need instant rollback (blue-green), need gradual (canary)