Mutable state becomes a security and correctness hazard as entities grow in complexity and are accessed concurrently. Race conditions can invalidate safeguards, while ad hoc “just an if” checks scattered across services or methods lead to inconsistent rules that are hard to audit and test. The chapter argues for consciously designing how state is represented and changed, selecting patterns that fit the runtime environment (single-threaded vs multithreaded) to reduce complexity while upholding integrity and availability.
For single-threaded contexts, two patterns help tame complexity. Partially immutable entities lock down attributes that should never change (such as a customer identifier), shrinking the number of “moving parts” and letting the compiler enforce invariants. Entity state objects make permissible behavior explicit by encapsulating state rules and transitions in a dedicated helper, with entities delegating checks at the start of public operations. This centralizes business constraints, improves testability and auditability, and closes loopholes like shipping unpaid orders or calling methods that should be forbidden in a given state.
In multithreaded settings, entity snapshots represent state with immutable read models and move mutations to domain services, improving throughput and avoiding contention, deadlocks, and accidental in-place changes; the separation also supports privilege boundaries for read vs write operations. When an entity’s state graph grows unwieldy, entity relay splits the lifecycle into one-way phases, each modeled as its own entity with a small, clear set of states and a simple handoff to the next. Used where phases don’t loop back and transition points are few, these patterns collectively reduce conceptual overhead and strengthen integrity and availability.
Figure 7.1. Entities should behave in a way that’s appropriate to their state.
Figure 7.2. A series of photos gives an impression of a real person, even if you never meet up in real life.
Figure 7.4. Civil status: single, married, divorced, widow/er, dead
Figure 7.5. The book order can be in a lot of distinct states, each with its own rules and constraints.
Figure 7.6. The life of a person viewed in two ways: as a single entity and as a chain of entities.
Figure 7.7. A book order passes through different phases during its lifespan.
Figure 7.8. A book order as a chain of three entities
Summary
- Entities can be designed to be partially immutable.
- State handling is easier to test and develop when extracted to a separate object.
- Multithreaded environments for high capacity require a careful design.
- Database locking can put a limit on availability of entities.
- Entity snapshots are a way to regain high availability in multithreaded environments.
- Entity relay (when fulfillment of one entity gives rise to another) is an alternative way to model an entity that has lots of different states.
FAQ
Why is mutable state in entities risky, especially with concurrency?
Mutable state can lead to race conditions when two threads modify the same entity concurrently. Classic symptoms include business rules being bypassed (for example, two overlapping withdrawals each “see” sufficient balance) or ending up with an incorrect final state. As entities grow more complex—with many states and transitions—the likelihood of subtle, integrity-breaking bugs increases unless the design explicitly controls change.How should I design entities differently for single-threaded vs multithreaded environments?
In single-threaded setups (for example, inside an EJB container or an actor like Akka), you can rely on the environment to ensure only one thread touches an entity at a time. In multithreaded scenarios (for example, shared caches like Memcached), entities may be accessed concurrently; naïve synchronization (synchronized, locks) harms capacity and risks deadlocks. Choose patterns that fit the environment: state objects in single-threaded contexts; snapshots in multithreaded contexts.What does it mean to make an entity partially immutable, and when should I do it?
Partial immutability means locking down fields that should never change after construction (for example, Order.custid). You set them once in the constructor and use immutable value types. Benefits: - Integrity by design (no “kidnapping” an order by swapping customer IDs) - Compiler-enforced safety (attempted mutations fail to compile) - Simpler reasoning, fewer “moving parts”What are entity state objects and how do they help?
Entity state objects encapsulate the rules for what actions are allowed in a given state. The entity delegates to this helper at the start of each public method to validate the call (for example, MaritalStatus.date() rejects dating when married). Benefits: - Centralized, explicit state rules (not scattered “if” guards) - Better testability (small, focused tests for state transitions) - Easier auditing of allowed/forbidden actionsHow do I know I need a state object instead of scattered checks?
Warning signs: - Rules live in services, not in entities (“it’s just an if statement”) - Many conditional guards sprinkled through methods - Hard-to-audit behavior (you must grep the codebase to find rules) - Fragile tests that mock lots of dependencies Try drawing a state graph and naming states; if there are at least two states with non-trivial rules/transitions, extract a state object.What is the entity snapshot pattern and why is it useful?
An entity snapshot is an immutable view of an entity at a point in time (for example, OrderSnapshot). It can contain read-side domain logic and invariants but no mutating methods. Advantages in multithreaded/high-read systems: - High availability and throughput (no object-level locks on reads) - Integrity (immutable snapshots cannot drift into invalid states) - Reduced contention vs fetching mutable entities that induce DB locks (and even page-level locks)If snapshots are immutable, how do I change the underlying entity?
Use a domain service with command-like methods (for example, addOrderItem) that: - Validate business preconditions - Persist the update (SQL or ORM) This cleanly separates reads (snapshots) from writes (commands), aligning with CQRS and the Single Writer Principle. Reads stay fast and concurrency-friendly; writes are controlled and validated.What security benefits do these patterns provide?
- Partial immutability: prevents illegitimate changes to critical identity/ownership fields (integrity).- State objects: enforce “allowed in this state” rules consistently (integrity).
- Snapshots: keep read paths lock-free and safe while preventing unauthorized mutation in read scenarios (availability and integrity).
- Relay: reduces state complexity, lowering the chance of exploitable inconsistencies (integrity).
What is an entity relay (entity chain), and when should I use it?
Entity relay splits a complex lifecycle into phases modeled as separate entities that pass the baton forward (for example, PreliminaryOrder → DefinitiveOrder → RejectedOrder). Use it when: - A single entity has too many states (~10 or more) - Phases are mostly one-way (no going back to earlier phases) - Transitions between phases are simple and few (ideally one)When should I avoid relay or snapshots, and what if my state graph is a mess?
- Avoid relay if the entity has few states, there are loopbacks to earlier phases, or many different birth points for the next phase (the simplicity is lost).- Avoid snapshots if your environment is already single-threaded or read/write loads don’t justify the split.
- If your state graph looks like a tangled ball of yarn, step back and remodel with domain experts: challenge complexity drivers, trim unnecessary paths, and seek a clearer, smaller set of states.
Secure by Design ebook for free