Skip to content

Reusable shapes for building services out of containers and cooperating processes, from a container-patterns reading. They sit between the building blocks and the full designs: Building Blocks, Data and Consistency, Worked Designs. Runtime side: Docker and Kubernetes Pods in Running Containers in Kubernetes.

At a glance

Group Pattern One-line idea
Single node Sidecar Helper container beside the app, sharing its lifecycle
Single node Ambassador Helper that talks to the outside world on the app's behalf
Single node Adapter Helper that makes the app look like a standard interface
Serving Replicated, load-balanced Many identical copies behind a balancer
Serving Sharded Each copy owns part of the data
Serving Scatter/gather Fan a request out, merge the replies
Serving Functions (FaaS) Short-lived code run per event
Serving Ownership election Exactly one replica owns a job at a time
Batch Work queue Shared queue, pool of workers
Batch Event-driven batch Chain of stages triggered by events
Batch Coordinated batch Join and reduce results from parallel stages

Single-node patterns

All three run in the same Pod / host as the main container, so they share network and disk and are scheduled together. The point is a clean boundary: each container does one job and can be reused or replaced alone.

flowchart LR
    subgraph Pod
    A[App container]
    S[Sidecar adds a feature]
    AM[Ambassador proxy out]
    AD[Adapter normalises output]
    end
    A <--> S
    A -- localhost --> AM --> EXT[(Remote service / shards)]
    A --> AD --> MON[Monitoring system]

Sidecar

  • Adds a capability without changing the app: HTTPS in front of a legacy HTTP-only service, log shipping, config refresh, a git-sync that keeps files up to date.
  • Example: an old web app only speaks HTTP. A sidecar running Nginx terminates TLS on 443 and forwards to localhost:8080.
  • Dynamic config: a sidecar polls a config service and rewrites a shared file or signals the app, so config changes need no redeploy.
  • Design each container with a small, documented API (env vars, ports, volumes) so it is reusable.

Ambassador

  • A proxy that hides the messy outside world from the app. The app always talks to localhost.
  • Sharding ambassador: the app thinks it talks to one Redis; the ambassador routes each key to the right shard.
  • Service brokering: it finds which backend to use, so the app has no discovery code.
  • Request splitting / experiments: send 5% of traffic to a new version, or mirror requests for testing.
  • Dev vs prod: same app code, different ambassador config.
$ app -> redis://localhost:6379        # the app's view
$ ambassador -> shard 1 / 2 / 3        # picks one by hashing the key

Adapter

  • Converts the app's own interface into the one everything else expects.
  • Monitoring: an app exposes custom stats; an adapter reformats them as Prometheus metrics.
  • Logging: adapters normalise many log formats into one before shipping.
  • Health: an adapter runs a deeper check (does a query really work?) and exposes a simple /healthz.

Serving patterns

Replicated, load-balanced services

Stateless copies behind a balancer (Building Blocks). Scales requests per second and survives failures.

  • Readiness probe: the balancer only sends traffic to a replica that reports ready. "Alive" (process running) and "ready" (can serve) are different checks.
  • Session tracking: if a user must reach the same replica, use sticky sessions (hash on IP or cookie). The cost is uneven load and loss of the session if that replica dies; prefer moving session state into a shared store so replicas stay stateless.
  • Caching layer: put a cache (Nginx, Varnish, Redis) in front of the replicas. Run it as its own replicated tier so it scales separately. Stale data is the price.
  • SSL termination: let the balancer or a sidecar decrypt TLS once, so the app servers do plain HTTP inside the network.
  • Rate limiting / DoS defence: a layer in front that throttles per client (Reliability Patterns).
flowchart LR
    C[Clients] --> LB[Load balancer readiness probes]
    LB --> CA[Cache tier]
    CA --> R1[Replica 1]
    CA --> R2[Replica 2]
    CA --> R3[Replica 3 - not ready]
    LB -. skipped .-> R3

Sharded services

Replication scales requests; sharding scales data size. Each shard holds a slice and a router picks the shard from a key.

flowchart TD
    C[Client] --> RT{"Router / ambassador shard = hash of key mod N"}
    RT --> A[Shard A: replicated]
    RT --> B[Shard B: replicated]
    RT --> D[Shard C: replicated]
  • Sharded cache: the dataset is too big for one cache's memory, so split it. Losing one shard only drops a slice of the cache, though the database then takes that load.
  • Replicated + sharded: each shard has several replicas, to survive failures and share load.
  • Good sharding function: deterministic (same key, same shard), uniform (even spread), and consistent hashing so resizing moves few keys (Data and Consistency).
  • Pick the key to match access patterns (user ID, not timestamp, or one shard takes all new writes).
  • Hot shards: one popular item (a viral photo) overloads one shard. Add replicas to that shard only, and move cold shards together. Autoscale per shard.

Scatter/gather

Scale for time: split one request into parts, run them in parallel, merge. Latency becomes the slowest part, not the sum.

flowchart TD
    C[Client] --> R[Root]
    R --> L1[Leaf 1: shard of docs]
    R --> L2[Leaf 2: shard of docs]
    R --> L3[Leaf 3: shard of docs]
    L1 --> R
    L2 --> R
    L3 --> R
    R --> C
Variant How it works Example
Root distribution Every leaf can do the whole job; the root just splits work Rendering image tiles, a computation split by range
Leaf sharding Each leaf holds part of the data; the root asks all of them Distributed search: each shard searches its own documents, root merges the top results
  • Search example: query "error 500" goes to 20 index shards, each returns its best 10, the root keeps the overall best 10.
  • Tail latency: the slowest leaf delays everything. Use timeouts, replicas and hedged requests (ask two replicas, take the first answer); return partial results rather than fail.
  • Make the root replicated too, so it is not a single point of failure.

Functions and event-driven processing (FaaS)

Code runs only when an event arrives and scales to zero between events. See also AWS Compute (Lambda).

Good fit Poor fit
Rare or spiky events, small stateless steps Long-running work, large in-memory state, constant high load (always-on is cheaper)
  • Benefits: no servers to manage, pay per run, simple scaling.
  • Challenges: cold starts, limited run time, no local state (use external storage), hard to debug locally, vendor lock-in.
  • Patterns: a decorator function (add auth/logging around another), webhook handlers, event pipelines (function chains where each output triggers the next), scheduled jobs.
  • Example: upload an image to storage, which fires a function to make a thumbnail, which fires another to update the database.
flowchart LR
    U[Upload to storage] --> F1[fn: resize] --> F2[fn: update DB] --> F3[fn: notify user]

Ownership election

Some jobs must have exactly one owner (running a nightly report, handling one customer's data, a primary node). Replicas elect one.

  • Do not write Paxos or Raft yourself. Use a replicated store that offers it: etcd, ZooKeeper, Consul.
  • Build a lock on top: create a key if it does not exist, with a lease/TTL. The holder renews it; if it dies, the lease expires and another replica takes over.
  • Fencing / versioning: a slow ex-owner may still think it owns the job. Attach a version number (compare-and-swap), so stale writers are rejected.
  • Do you need it? Prefer designs where concurrent work is safe. A database transaction or an idempotent job is often simpler than election.
sequenceDiagram
    participant A as Replica A
    participant B as Replica B
    participant K as etcd / ZooKeeper
    A->>K: create lock (TTL 10s)
    K-->>A: ok, you own it
    B->>K: create lock
    K-->>B: exists, not yours
    A->>K: renew every 5s
    Note over A: A crashes
    K-->>K: TTL expires
    B->>K: create lock
    K-->>B: ok, you own it now

Batch patterns

Work queue systems

Jobs go into a shared queue; a pool of identical workers pulls and processes them. Scale by adding workers.

flowchart LR
    SRC[Source: items to process] --> Q(["Work queue"])
    Q --> W1[Worker 1]
    Q --> W2[Worker 2]
    Q --> W3[Worker 3]
    W1 --> OUT[(Results)]
    W2 --> OUT
    W3 --> OUT
  • Generic design: a source container produces work items; a worker container implements run(item); the queue infrastructure is shared and reusable.
  • Dynamic scaling: scale workers on queue length (KEDA, autoscaling).
  • Reliability: a worker takes a lease on an item; if it dies the item reappears. Process items idempotently.
  • Multi-worker pattern: chain several different worker containers on the same item (for example, resize, then watermark), each a separate, reusable image.

Event-driven batch processing

A pipeline of queues: each stage reads events, does one step, writes events for the next. Useful building blocks:

Stage type What it does Example
Copier Duplicate one stream to several consumers Send each order to billing and shipping
Filter Pass only matching items Keep only paid orders
Splitter Route items to different streams by rule Split by region or priority
Sharder Spread items across N identical workers by key Hash on customer ID
Merger Combine several streams into one Collect events from many sources
flowchart LR
    IN[Events in] --> F[Filter] --> SP{Splitter}
    SP -- EU --> SH1[Sharder] --> WA[Workers EU]
    SP -- US --> SH2[Sharder] --> WB[Workers US]
    WA --> M[Merger] --> OUT[Output]
    WB --> M
  • Publisher/subscriber infrastructure (Kafka, SNS, Pub/Sub) carries the events so stages stay independent.
  • Each stage can fail and restart without the others noticing, which is why queues are used between stages.

Coordinated batch processing

When parallel work must be brought back together.

  • Join: wait until all parallel branches of a stage finish before starting the next one (a barrier).
  • Reduce: combine many partial results into one. Reduce is done in steps so no single node is overloaded (a tree).
  • Example, word count / histogram:
  • Split a big file across 100 workers (the map step).
  • Each worker counts its words and emits a partial count.
  • Reducers merge counts for each key (reduce), and finally one reducer sums the totals.
flowchart TD
    D[Big dataset] --> M1[Map 1] & M2[Map 2] & M3[Map 3]
    M1 --> R1[Reduce A-M]
    M2 --> R1
    M3 --> R1
    M1 --> R2[Reduce N-Z]
    M2 --> R2
    M3 --> R2
    R1 --> J[Join: wait for all]
    R2 --> J
    J --> F[Final result]
  • Rule of thumb: make each step idempotent and store intermediate output durably, so a failed worker only repeats its own slice.

Choosing a pattern

Need Reach for
Add TLS / logging / config sync to an old app Sidecar
Hide shards, discovery or A/B routing from the app Ambassador
Expose metrics in a standard form Adapter
More requests per second Replicated, load-balanced
More data than one machine holds Sharded
Lower latency of one big request Scatter/gather
Rare, spiky, short tasks Functions
Exactly one worker may act Ownership election
Lots of independent jobs Work queue
Multi-step flow with branching Event-driven batch
Results must be combined Coordinated batch (join, reduce)