Software Design Reference System Designer · Managing Complexity

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.
Field notes · what it looks like in real code
You see thisWhat it costsThe move
obj.field = value from outside the classEvery writer must remember the rules; one of them won’t.A method on the owner: obj.change_x().
a dict mutated by three functionsNo owner, no invariants, order-dependent results.Return new values, or give the dict an owning class.
copy.deepcopy sprinkled defensivelyA symptom of shared mutable state, not a cure for it.Hand over a frozen value object instead.
two classes with a sync() fixing each otherOwnership 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

Bring a two-writer object to your agent-teacher and design the ownership split together.