Skip to content
aipatterns.dev

Category

Data Patterns

Patterns for keeping application data honest at speed and across services: copies that exist only to be lost, multi-step transactions with explicit undos, and messages that cannot be lost or doubled.

What these patterns solve

An application that scales up its reads eventually holds data in more than one place, and every extra copy is a way to be wrong. A workflow that spans services cannot share a transaction, and a state change that must notify the world has two ways to fail - silently losing the message, or delivering it twice. These patterns fix the division of labor: the store decides the truth, the copies accelerate it, workflows get all-or-nothing semantics without locks, and messages become data before they become side effects.

The rule they all enforce: writes land in the source of truth first; anything else is a disposable speedup - and every copy or message says exactly how stale or how repeated it may be.

Members

  • Cache-Aside: load into a cache on miss, save to the store and evict on write - so reads speed up while the store stays the only source of truth.
  • Transactional Outbox: write the outgoing message inside the business transaction; a relay publishes it afterwards - the commit and the message are one fact.
  • Saga: run a multi-service workflow as local steps with compensations, undone in reverse on failure - consistency without distributed locks.
  • Idempotent Consumer: claim each message id in the processing transaction, so at-least-once delivery produces exactly-once effects.
  • CQRS: separate the command model from per-view read models joined by projections - each shaped for its job, at the price of two models and read lag.

How to choose

  • Reads dominate, the query is expensive, and slightly stale answers are acceptable: Cache-Aside. Give every entry a TTL and evict on write - never write to both copies and hope.
  • A state change must reliably produce a message: Transactional Outbox. The outbox row commits with the change; never send from inside or just after a transaction.
  • A workflow spans services and must be all-or-nothing: Saga. Name every compensation first; a step without one goes last or becomes retriable.
  • Delivery is at-least-once anywhere in the path: Idempotent Consumer on every counting handler - outbox relays, webhook providers, and queue redelivery all guarantee duplicates.
  • Reads and writes measurably fight over one model, on one contentious subdomain: CQRS. Elsewhere, CRUD; be very cautious.
  • The value must always be current for correctness and lives in one store: none of these. Read the store.

Prefer no pattern when

  • The store is in-process and already fast - a map beside your map is not a cache layer.
  • Writes are as frequent as reads; the invalidation overhead cancels the read savings.
  • The whole workflow fits one database transaction - a saga re-implements it without the isolation.
  • You have not measured the query you are about to cache, or the contention you are about to split.

See also

Original writing and examples. Topic coverage follows Microsoft Azure Architecture Center, which explains this topic with its own illustrations.