# 09. State

How to design state, lifecycles, state machines, optimistic locking, and eventual consistency — without magic and without accidental flags.

- **Author:** Fedor Chishchin
- **Level:** advanced
- **Reading time:** ~25 minute read
- **Published:** 2026-05-23
- **Source:** https://fedorthinks.com/learn/software-architect-09-state

---

## 1. The core idea

If an object has a lifecycle, design it as a state machine — not as a pile of boolean flags.

### State has to be explicit

> A traffic light is understandable because it has a limited set of states and permitted transitions. If all lamps lit at once, drivers would be arguing in the intersection.

Order, payment, delivery, subscription, ticket, deployment — these are entities with a lifecycle. They have states and transitions: `draft`, `pending_payment`, `paid`, `cancelled`, `refunded`. The architectural work is to define the allowed transitions and the owner of each one.

When you get flags like `isPaid`, `isCancelled`, `isRefunded`, `isArchived` instead of a state machine, the system quickly reaches impossible combinations: paid and cancelled at the same time, delivered without payment, refund without capture.

### Concurrency is part of the model

> If two people edit one document at the same time with no rules, the winner isn't whoever's right — it's whoever saved last.

In a real system several processes can change the same state: the user, a worker, a webhook, an admin, a retry. You need optimistic locking, a version field, transactions, unique constraints, or compare-and-swap operations.

The architect has to ask: who is allowed to change this state, how are races resolved, which operations are idempotent, and what counts as a final state.

## 2. Example and anti-example

Good state can be explained with a diagram. Bad state has to be explained with a list of exceptions.

### Example: payment lifecycle

> A bank operation goes through stages. You can't first return the money and then decide whether it was charged.

A payment can move from `created` to `authorized`, then to `captured` or `voided`. A refund is only possible after `captured`. The provider's webhook doesn't write arbitrary fields — it triggers a transition that checks the current state and version.

That approach makes edge cases part of the model, not a pile of `if` statements scattered across the code.

### Anti-example: flags instead of a lifecycle

> A form with ten checkboxes feels flexible — until two of them contradict each other.

The order has `paid_at`, `cancelled_at`, `failed_at`, `refunded_at`, `completed_at`, but no single clear status. Different parts of the code interpret the combinations differently. Support sees one thing, accounting sees another, the API sees a third.

That isn't flexibility. It's an implicit state machine with no rules.

#### Self-check

- Which states does this entity have?
- Which transitions are forbidden?
- Who owns the transition: the user, the system, or an external provider?
- How do we protect against retries and races?

Next module: [Distributed systems](./software-architect-10-distributed-systems).
