How would you decide between one application-wide store and many small independent stores, in a long-lived application?
answer
- invariants first, layout second
- atomicity, granularity, isolation
- slices inside one instance
- measure wakes per interaction before migrating
basics
~20 sDecide by invariants, not by size: values that must change together belong behind one applier, independent concerns belong in separate stores. Then weigh notification granularity, per-request instancing, ownership and migration cost, and measure before moving.
solid answer
~50 sI would not decide by size. The first question is which values must stay consistent with each other: a change spanning two stores is two writes, and between them a subscriber can read a pair that should not exist, so anything with a cross-field invariant belongs behind a single applier. Genuinely independent concerns — a theme choice, an open panel, a draft — do better as small stores, because each subscription then wakes for one thing by construction and each piece can be created, tested and disposed of on its own. For most long-lived applications the answer is in the middle: one store assembled from independently defined slices, each owning its write surface, with one instance per request. Before proposing a migration I would measure how many subscriptions wake per interaction, how often a change spans slices, and how many call sites write each slice.
go deeper
Recall the two shapes and the basic trade: one store gives one place to look and one atomic change; many small stores give narrow subscriptions and independent lifetimes.
Explain why a change spanning two stores is observable between its two writes, and why one store means every subscriber is notified per write so narrowness has to come from selectors.
Show the diagnosis: measure woken-but-no-op subscriptions per interaction and the share of changes that span slices, then distinguish a selector problem from a genuine layout problem.
Own the invariant boundaries and the staging plan. Commit to the middle shape deliberately, name what review must enforce, and stop the migration when the measurements stop improving rather than when the shape looks finished.
## The question behind the question "One store or many" sounds like a question about file layout. It is not. Every genuine difference between the shapes comes from three things: which changes must be **atomic**, which subscribers must be **woken**, and which instances must be **isolated**. Decide those and the layout follows. Decide the layout first and you spend a year arguing about consequences. ## The axes | Axis | One store | Many small stores | |---|---|---| | Cross-field invariants | one action, one notification, never observed half-applied | two writes, and a subscriber can read the gap | | Notification granularity | every subscriber woken per write; narrowness comes from selectors | a subscription wakes for one concern by construction | | Instancing and tests | one factory, one thing to create per request or test | many small things, each trivial to create and dispose | | Discoverability | one place to look and one action vocabulary | blast radius is obvious, location less so | | Coupling | easy for one feature to read another's fields | a cross-feature read is an explicit edge | | Derived state | derivations sit inside one applier | derived stores form a dependency graph | | Migration cost | large, hard to stage | incremental, one feature at a time | ## When one store earns it - Fields carry **invariants that span them** — a selection that must be cleared when the list it points into is replaced, a total that must match its line items. One applier makes that one step, so no subscriber ever reads the inconsistent pair. - You need to answer **"what changed this, and in what order"** for the whole application from a single ordered log. - You want exactly **one thing to create and provide** per request or per test. ## When many small stores earn it - The concerns are genuinely **independent**, so nothing about one constrains another. - Subscriptions should be narrow **by construction** rather than by everyone remembering to write a careful selector. - Features are **owned by different teams** and should not reach into each other's fields casually. - Pieces need **independent lifetimes** — created with a screen, discarded with it. ## The middle shape, which is usually the answer One store **assembled from independently defined slices**, each owning its write surface and its own invariants, instantiated once per request or test. You keep a single thing to create and provide, one ordered change log, and atomicity available when a change spans slices; subscriptions stay narrow because each slice is selected separately. The failure mode to watch is a slice reaching directly into another slice's fields — the coupling the many-stores shape prevents structurally and this shape can only prevent by review. ## What to measure before proposing a move 1. **Subscriptions woken per interaction.** Instrument the notification path and count listeners that ran against listeners that actually scheduled an update. A high woken-but-no-op ratio is a selector problem, and consolidating stores will not fix it. 2. **How often a change spans slices.** If nearly every action touches one area, atomicity is buying little. If a third of them span two, the split is costing correctness. 3. **Write sites per slice.** Many scattered writers to the same slice argue for a named write surface, whichever shape holds it. 4. **What breaks in tests.** Order-dependent failures point at shared instances, which is a packaging problem neither shape fixes by itself. Numbers matter here because both shapes have loud advocates and most of the published arguments are aesthetic. ## Failure modes at the extremes - **One store, no selector discipline:** everything selects broadly, every write wakes everything, and the team reaches for ever-wider comparisons instead of narrower selections. - **One store, no boundaries:** features read each other's fields until the store is the coupling surface nobody can refactor. - **Many stores, shared invariants:** values that must agree live apart, and the product grows defensive code for states that should have been impossible. - **Many stores, deep derivation:** derived stores read derived stores, ordering becomes observable, and eventually a cycle appears. ## How to stage a migration Keep both alive for a while. Define slices with their own write surfaces inside whichever container you are moving toward, move one feature at a time behind its slice's surface, keep exactly one instance per request so no component can see two sources of truth, and re-measure after each move. Stop when the numbers stop improving: the goal was never the shape, it was atomicity where invariants demand it and narrow notification everywhere else.
- What do derived stores cost as the number of small stores grows?The dependency edges between them become a graph. A change can traverse several derivations before reaching a subscriber, evaluation order becomes observable, and cycles become possible. Keep derivations shallow and one-directional, and treat two stores that each read the other as a design error rather than something to order correctly.
- A team argues one store is 'cleaner'. What would you ask for instead?A named invariant that currently spans stores and has produced a bug, plus two measurements: subscriptions woken per interaction and the share of changes that span slices. Without those, the proposal is aesthetic, and consolidation tends to trade a coupling problem for a notification problem.
- Does the one-or-many choice affect how the store is instantiated?It changes the bookkeeping, not the rule. Whichever shape you choose, each request and each test needs instances it owns, so the exports are factories. One store means one factory to call and provide; many stores mean many, which is why a single creation point that builds them together is worth having.
saying these in an interview costs you the question
- Argues purely by size or tidiness without naming an invariant.
- Splits two values that must change together into separate stores.
- Puts everything in one store, then fights renders with wide comparisons.
- Builds derived stores that read each other in a cycle.
- Proposes a migration with no measurement of what wakes per interaction.
- Assumes one store automatically means narrower notifications.