advanced 3 min answer

Walk me through how you would size an aggregate in a marketplace of eBay's shape, where one seller can hold 80,000 active listings. Then tell me what your choice costs at runtime.

dddaggregateconcurrencyinvariantscontention
Show the full answer Hide the answer

What the interviewer is testing

Whether you know that an aggregate is a transactional consistency boundary, which makes its size a concurrency decision wearing the costume of a modelling decision. Candidates who treat it as modelling draw a pretty graph and ship a contention bug.

Make Seller the root with listings inside it and every listing edit takes the seller's lock or version. All 80,000 listings now serialise behind one row. For a power seller running a repricing job at 20 writes a second against a transaction that takes 15 ms, the symptom in production is a version-conflict retry rate climbing past 40% with flat CPU and flat database load — a retry storm, because each failed attempt returns to contend again. The system is busy retrying rather than working.

The clarifying questions that change the answer

  • Which invariants must hold in the same instant, and which may hold within a minute? This is the whole question, and it is a business question, not a technical one.
  • What is the write rate on the root, and is it uniform? A uniform 2 writes a second tolerates a large aggregate; a long tail of power sellers does not.
  • How large can the collection get, and is it bounded? An unbounded collection inside an aggregate is a load-the-world read waiting to happen.
  • Does anything need the whole graph for reading? If yes, that is a projection, and it is not an argument for a bigger aggregate.

A strong answer's arc

Put inside the aggregate only what the invariant needs in the same instant; everything else holds an id and reconciles asynchronously. Here that means Listing is its own aggregate and Seller holds no listing collection at all.

Then confront the invariant that resists. "A seller on the basic plan may hold at most 500 active listings" spans both. Two honest options, and naming both is the senior move:

  • Make the counter the aggregate. A small hot row holding plan and active count, incremented in the same transaction as the listing insert. Exact, and it reintroduces one serialisation point per seller — tolerable, because it is a counter update rather than a listing write.
  • Enforce eventually and compensate. Allow the insert, detect the breach asynchronously, deactivate the excess and notify. Cheaper, and it means the rule is violated for seconds.

Choose the counter aggregate when a breach has a cost that cannot be undone — a refund, a regulatory report, a published price. Prefer eventual enforcement with compensation when the breach is embarrassing rather than expensive. The underlying question is whether the business would rather reject a legitimate listing or briefly allow an illegitimate one, and that belongs to the person who owns the plan. Arriving with it already decided is the mistake.

What it costs

Small aggregates mean more aggregates per use case, so a change spanning two of them needs a process manager, an outbox or a saga, and you have given up a single transaction. You have traded a serialisation point you could see for a reconciliation path you now have to build, monitor and explain. The honest accounting is that contention was a performance problem and partial completion is a correctness problem, so the trade is only worth it where contention is real.

Common weak answers

  • "One aggregate per entity." A rule with no reference to invariants or write rates, which produces both oversized roots and aggregates that cannot enforce anything.
  • "Use eventual consistency." Which invariant is relaxing, for how long, and who cleans up? Without those three the phrase is a way of not answering.
  • Loading the 80,000-child aggregate and blaming the ORM. The ORM did what the model told it to.