skip to content

When a save or delete cascades along an object's links, what happens, and how can a cascade reach too far?

level: middleimportance: should knowfreq 55%

answer

  1. a transition that travels along links
  2. declared in the mapping, not per call
  3. composition yes, shared reference no
  4. orphan removal is a separate declaration
  5. store-level cascade is a different layer

basics

~20 s

A cascade applies one object's state transition to the objects reachable through links declared to cascade, so one save or delete becomes many. It reaches too far when it crosses a link to shared data that other rows still need.

solid answer

~50 s

Cascading means a transition is **propagated**: saving a parent pulls newly built children into the tracked set with it, and deleting a parent plans deletes for what it owns. Which links cascade is a **mapping declaration**, decided when the model is written, not a per-call choice - so the same save behaves the same way everywhere, for better and worse. It reaches too far when a cascading link points at data the object does not own: a shared lookup or reference row that other objects still point at, deleted because one holder went away. Cycles are the other hazard, and layers guard them by remembering which objects they have already visited. The safe rule is to cascade along composition - parts that cannot exist without the whole - and never along a link to something shared.

go deeper

for a junior

Know that a cascade means one save or delete is applied to linked objects too, and that it is set up in the mapping. The useful instinct is that parts of a whole may cascade and shared lookup rows must not.

for a middle

Explain which transitions travel, and why declaring a cascade changes every call path rather than one. Be ready to describe the over-broad delete across an association and how a cycle is handled.

for a senior

Show that you verify rather than trust: read the statements a representative write emits, and treat adding a cascade to an existing link as a change with a blast radius across the codebase.

for a principal

Decide where ownership lives in the model. Cascade boundaries are aggregate boundaries; if the model does not distinguish composition from association, no local cascade setting will keep deletes correct for long.

A cascade is a **state transition that travels**. When the layer applies save, delete or detach to an object, it walks the links the mapping marks as cascading and applies the same transition to what it finds, then repeats from there. One call at the top becomes a set of transitions across a graph. ## What cascading is for Object models have parts. An order has lines; a document has sections; a survey has questions. Those parts have no independent existence: they are created with the whole, they are deleted with the whole, and no other object refers to them. Writing save and delete calls for each of them by hand is noise, and the noise is where a forgotten child comes from. Cascading encodes the ownership once, in the mapping, and then the layer keeps the parts in step with the whole. ## Which transitions can travel | Transition | What cascading it means | Typical risk | |---|---|---| | Save | new objects reachable through the link join the tracked set | a large graph turns one call into many inserts | | Delete | reachable objects get deletes planned too | rows other objects still reference are removed | | Detach | reachable objects leave the set with the parent | later edits to a child silently go nowhere | | Refresh or reload | reachable objects are re-read from the store | unwritten edits in the graph are discarded | Layers differ in which of these they support and in how finely they can be declared, but the shape above is common. ## A cascade is a declaration, not a decision This is the point most often missed. The mapping says the link cascades; every call that touches that link then cascades, whether or not this particular call wanted it. Adding a cascade to a link is therefore a **global** change to how every write path behaves, and it should be reviewed like one. Some layers let a specific call opt out; relying on that is fragile, because the default is what most code will get. ## How it reaches too far 1. **A delete crossing a shared reference.** The link from an object to a category, a tag, a currency or a customer is an *association*, not ownership. Declaring delete-cascade there means removing one object removes the shared row, and every other object pointing at it now dangles or blocks on a foreign key. This is the single most common cascade accident. 2. **Cascades on both ends.** Parent cascades to child and child cascades back to parent, so the walk cycles. Layers guard by remembering visited objects, so this is usually a correctness non-event - but it makes the reachable set far larger than anyone intended, which turns into cost. 3. **A graph bigger than expected.** Save-cascade over an object that has accumulated a large loaded collection means the layer inspects and possibly writes all of it. The call still reads as one line. 4. **Orphans left behind.** Removing a child from a parent's collection is not the same as deleting it, and in most layers it is a *separate* declaration. Without that declaration the child's row survives, pointing at nothing. ## Mapping cascade versus the store's own cascade These are two mechanisms at two layers, and they are easy to conflate. A cascading delete declared in the **mapping** makes the layer emit a delete statement per child, which means the layer's tracked set stays consistent with what happened. A foreign key declared `ON DELETE CASCADE` in the **store** makes the database remove dependent rows itself, emitting nothing the layer can see - so in-memory objects for those rows are now stale, and the layer never knew. Both can be right; having both on the same relationship means the delete happens twice by two mechanisms, and the counted rows will not match anyone's expectation. ## Keeping it safe - Cascade **save** freely down composition, where the parts are created with the whole. - Cascade **delete** only where deletion of the whole genuinely means deletion of the part. Ask "could anything else point at this row?" - if yes, do not cascade. - Treat adding a cascade as a change to every write path, and read the statements a representative write actually emits afterwards rather than assuming. - Keep ownership visible in the model: if a link is genuinely shared, saying so in the mapping is what stops the accident.

  • Why is cascading save usually safer than cascading delete?
    Because the failure modes differ in kind. An over-broad save costs extra work and may write rows nobody needed, but the data stays coherent and the surplus is visible in the statements. An over-broad delete removes rows other objects still reference, which is either an immediate constraint failure or, worse, quiet data loss that only surfaces when something else tries to read what is gone.
  • Does removing a child from a parent's collection delete the child's row?
    Not by itself. Detaching the child from the collection changes the link, and in most layers deleting the now-parentless row requires a separate orphan-removal declaration or an explicit delete. Without one, the row survives with a dangling or null reference, which is how a table quietly accumulates unreachable rows.
  • If the store already declares ON DELETE CASCADE on the foreign key, should the mapping cascade too?
    Usually pick one. The store's cascade removes the child rows itself without telling the layer, so tracked objects for those rows are stale afterwards. The mapping cascade emits a statement per child, which is more statements but keeps the layer's picture accurate. Declaring both means the work happens twice by two mechanisms and the affected-row counts stop meaning what anyone expects.

saying these in an interview costs you the question

  • Thinks a cascade is chosen per call rather than declared
  • Says delete-cascade is safe on shared reference rows
  • Believes removing a child from a collection deletes its row
  • Confuses a mapping cascade with the store's own cascade
  • Assumes one cascading call costs one statement