Designing for failure: Patterns

Reusable building blocks to help design reliable systems in the presence of failures.

See the introductory post and the glossary for definitions of terms used throughout.

API design

Rather than internal details, these patterns describe the API as seen by clients.

Writing to a single system

Patterns for writing to a single system. Most patterns assume this system is an ACID database. This is the simplest topology, and the easiest to work with. It’s worth trying to design systems like this where possible, to avoid the complexity that arises from trying to maintain consistency between multiple systems.

Writing to multiple systems

When writing to a single ACID database, we get atomicity and consistency built in. Things get more complicated when writing to multiple systems where we don’t have these guarantees: we might not be able to perform all writes atomically, and so can end up in an inconsistent state.

Background processes

Sometimes inconsistency is unavoidable, whether by design, or simply because of a buggy implementation. Background processes can identify these inconsistencies and handle them in various ways.

Other

Other patterns for handling failure or edge cases.

Antipatterns

Some patterns exist which should be avoided. They may seem to offer benefits, but either do not deliver what they seem to or have other serious drawbacks.

Comparisons

When consistency is important, you will generally need to choose (at least) one of the patterns in the table below.

Pattern Systems Synchronicity Atomicity Isolation Consistency Complexity
ACID transaction One Sync Atomic Isolated1 Immediate1 Simple
Transactional outbox Many Async Non-atomic2 None Eventual Moderate
Reliable retries Many Async Non-atomic2 None Eventual Moderate
Completer Many Success: sync
Error: async3
Non-atomic2 None Eventual Moderate
Distributed transaction Many Success: sync
Error: async3
Atomic Partial4 Eventual Complex
Saga Many Async Atomic5 None Eventual Complex

More patterns

  1. Depending on the isolation level. See Transaction isolation in PostgreSQL. ↩ ↩2

  2. Non-atomic because some writes might fail with no guarantee that successful writes will be rolled back. ↩ ↩2 ↩3

  3. Attempts to do all work synchronously, but will continue asynchronously in the case of failure. ↩ ↩2

  4. Other operations never see changes that get rolled back, but while the systems are committing they can see one system’s changes before another’s. ↩

  5. Using compensating actions rather than rollback. ↩