Lesson 0020 · System Designer · Module 2
Shared State and State Ownership
Lesson 0019 removed the globals. This one handles the harder case: state that must be shared, but must have exactly one owner who is responsible for it.
Mission tie-in: "manage shared state and understand state ownership" — the rule that keeps concurrency bugs from becoming your daily life.
Knowledge: one owner per state
Every piece of state has exactly one owner: the object (or module, or process) responsible for its correctness — for its invariants (lesson 0011), its lifecycle, and its writes. Everyone else is a reader or a requester.
The failure is state with no owner — shared and edited by whoever happens to hold it:
# two modules both "own" the user's profile
class ProfileService:
def update_email(self, user: UserProfile, new_email: str) -> None:
user.email = new_email # writes directly
class SyncService:
def sync(self, user: UserProfile) -> None:
if user.email.endswith("legacy.com"):
user.email = user.email.replace("@legacy.com", "@new.com") # also writes
# whichever runs last wins; nobody validated the other's write
Two writers, no coordination: the profile's invariants (unique email, verified flag) can be broken by either path, and tests pass or fail depending on call order — the same disease as lesson 0019, now in objects.
Ownership made explicit:
class UserProfile:
def __init__(self, email: str, verified: bool) -> None:
self._email = email
self._verified = verified
def change_email(self, new_email: str) -> None: # the owner's door
if self._verified:
raise ProfileLockedError()
self._email = new_email
class ProfileService:
def update_email(self, profile: UserProfile, new_email: str) -> None:
profile.change_email(new_email) # requests; doesn't grab
class SyncService:
def sync(self, profile: UserProfile) -> None:
if profile.email.endswith("legacy.com"):
profile.change_email(profile.email.replace("@legacy.com", "@new.com"))
Both services still change the email — but only through the owner's door, which enforces the invariant and owns the write. That's the definition of ownership: the object guards its own state; outsiders request.
† Handoff over sharing: when you can, hand immutable snapshots instead of shared mutable objects — "value objects as messages" (lesson 0012). Sharing is for state that must be live; snapshots are for everything else.| You see this | What it costs | The move |
|---|---|---|
| obj.field = value from outside the class | Every writer must remember the rules; one of them won’t. | A method on the owner: obj.change_x(). |
| a dict mutated by three functions | No owner, no invariants, order-dependent results. | Return new values, or give the dict an owning class. |
| copy.deepcopy sprinkled defensively | A symptom of shared mutable state, not a cure for it. | Hand over a frozen value object instead. |
| two classes with a sync() fixing each other | Ownership is contested; each undoes the other’s work. | Name the single owner; the other becomes a reader. |
Skill: who's the owner?
Two services write a user's email directly. The defect is:
The fix is to route writes through:
When sharing isn't required, prefer:
Practice on your own code
Pick a shared mutable object in your codebase (a profile, a session, a document). List its writers. If there is more than one, route them through the object's own methods — then ask whether an immutable handoff would serve better.
Reveal: an ownership audit
A Cart object is mutated by CartView (adds items) and PriceService (rewrites prices). Audit: the view owns line composition; pricing owns prices. Split: Cart.add_line() guarded by the cart; pricing returns a new priced cart snapshot instead of editing in place. Two owners, zero overlap.
Your win
You can run an ownership audit on any shared state, route writes through the owner's door, and choose immutable handoffs where sharing isn't needed.
Read and watch deeper
- A Philosophy of Software Design, Ousterhout — ch. 9 "Better Together or Better Apart": deciding what should share what.
- Designing Data-Intensive Applications, Kleppmann — ch. 7 "Transactions": ownership and coordination when the state outlives a process.
- Robust Python, Viafore — the chapters on state management in Python systems.
- Watch: ArjanCodes YouTube — search "shared state" or "state management".
- Next: lesson 0021 — error handling strategies.
- Reference: Design Principles — tell, don't ask; glossary — shared state.
Bring a two-writer object to your agent-teacher and design the ownership split together.