Take a whole-part relationship where the part is owned by the whole and dies with it, and assume the in-memory expression of that ownership is already settled. The same claim still gets restated in three other places: the persistence mapping's delete rules (JPA's `cascade` and `orphanRemoval`, Django's mandatory `on_delete` argument, Entity Framework Core's required-versus-optional dependents), the disposal chain (C# `using`/`IDisposable`, Java try-with-resources, Python context managers), and the serialization format (embedding the part's body versus emitting its id). What concretely breaks when those three disagree, and which one do you make authoritative?
answer
- Claim restated in mapping, disposal, wire
- Django collects in Python; no ON DELETE CASCADE in DDL
- cascade ≠ orphanRemoval
- leaveOpen: ownership as a parameter
- Embedded twice = forked on read
basics
~20 sOwnership is restated in the mapping's delete rules, in who closes the part, and in whether the wire format embeds it or references it by id. Three engines enforce those independently, so they drift. Make one authoritative and derive the others.
solid answer
~50 sOne ownership claim, three enforcement engines that nobody keeps in sync. - **Persistence.** Django refuses a `ForeignKey` without `on_delete`, then cascades in Python via its collector — it does not put `ON DELETE CASCADE` in the constraint. JPA splits the claim across two flags: `cascade = ALL` propagates operations applied to the parent, while only `orphanRemoval = true` deletes a child detached from the collection. EF Core infers it from nullability — required dependents default to cascade, optional ones to setting the FK null. Same diamond, three different runtimes. - **Disposal.** Under garbage collection, who calls close is the only executable ownership statement. .NET makes it a parameter (`leaveOpen: true`); Java's decorators hardcode it — `BufferedReader.close()` always closes what it wraps. - **Wire.** Embedding declares ownership; an id declares an association. Embed a shared part in two documents and it forks into two objects on read. Make the persistence mapping authoritative — it is the layer that destroys data — and derive disposal and embedding from it.
code
text · 11 linesOWNED PART
model Order <>-- OrderLine
mapping delete Order => delete OrderLine [engine: ORM / DB]
disposal Order closes nothing (plain values) [engine: runtime]
wire OrderLine embedded inside Order body [engine: serializer]
SHARED PART
model Playlist o-- Track
mapping delete Playlist => Track survives (no cascade!)
disposal neither Playlist closes Track
wire Track emitted as { id }, body sent oncego deeper
Know the two concrete rules: cascading a delete destroys the child rows, and closing a resource is somebody's single responsibility. Be able to say why a part shared by two wholes must not be cascaded.
Explain the JPA cascade versus orphanRemoval split and what an orphan row is, and know that embedding a part in a document is a stronger statement than emitting its id.
Name where each layer executes — Python collector versus SQL constraint versus entity manager — and diagnose the resulting bugs: bypassed hooks on bulk deletes, closed borrowed handles, silently duplicated parts on deserialize.
Frame it as choosing a single authoritative layer and deriving the rest, argue for the persistence mapping on the grounds that it destroys data, and reason about the asymmetry between under-claiming (recoverable garbage) and over-claiming (unrecoverable deletion).
## The part of the claim that leaves the object graph Assume the in-memory side is settled: whatever your language uses to say "this part belongs to this whole" has been chosen. The claim does not stop there. The same relationship is written down again, independently, in at least three other places, each with a different enforcement engine and a different failure mode: 1. the **persistence mapping** — the rules deciding what a delete takes with it; 2. the **disposal chain** — who calls close/dispose on the part; 3. the **serialization boundary** — whether the part travels as an embedded body or as an identifier. Nothing keeps these three in agreement. They are usually written by different people on different days, and only the first one is visible when data disappears. ## Layer 1: the persistence mapping This is where the ownership claim is most literally executed, and the frameworks diverge in *what runs and where it runs*, not merely in syntax. - **Django (Python, 2.0+)** makes the decision mandatory: `ForeignKey` will not accept a definition without `on_delete`, so `CASCADE` / `PROTECT` / `RESTRICT` / `SET_NULL` is chosen at declaration time. Crucially, Django does *not* emit an `ON DELETE CASCADE` clause into the foreign-key constraint. Its collector loads the dependent rows into Python, deletes them, and fires `pre_delete`/`post_delete` signals. That means cascades cost queries and honour application logic — and any SQL run outside the ORM cascades nothing. - **JPA/Hibernate (Java)** splits one claim across two independent flags. `cascade = CascadeType.ALL` propagates operations performed on the parent; `orphanRemoval = true` deletes a child that has been detached from the parent's collection. Setting only the first is the classic defect that leaves orphan rows. Bulk JPQL `DELETE` bypasses both, and `@OnDelete(OnDeleteAction.CASCADE)` pushes the rule down into DDL instead — so the same annotation set can mean "the JVM deletes" or "the database deletes". - **EF Core (C#)** infers ownership from nullability: a required dependent defaults to cascade delete, an optional one to severing the reference. It also offers owned entity types, which have no key of their own and no root set — composition raised into the metamodel rather than expressed as a delete rule, which makes a two-owner part unrepresentable. - **Rails (Ruby)** gives the same diamond two runtime meanings: `dependent: :destroy` instantiates each child and runs its callbacks; `dependent: :delete_all` issues one statement and skips them. ## Layer 2: the disposal chain Under tracing collection, memory ignores your model — but sockets, file handles and pooled connections do not. The only executable ownership statement left is *who closes the part*, and the rule is that exactly one holder does. A whole that merely received a shared part must close nothing. The divergence here is whether ownership can be stated at all. .NET makes it a parameter: constructing a reader with `leaveOpen: true` says "I wrap something I do not own". Java's decorators hardcode the opposite — closing a wrapper always closes the wrapped stream, so passing a shared stream into one requires a non-closing shim. Python's `zipfile.ZipFile` closes the underlying file only if it opened it, encoding "I own what I created" as behaviour. Rust removes the decision: destruction runs when the owner goes out of scope, and there is no annotation to forget. ## Layer 3: the serialization boundary Embedding a part's body is an ownership claim; emitting its id is an association. This layer is usually decided for payload-size reasons and then silently contradicts the other two. A part shared by two wholes, embedded in both documents, comes back as two distinct objects; edits to them diverge, and a later deduplication by value merges rows that were meant to differ. Serializers differ on whether identity survives at all: .NET's reference-preserving mode emits `$id`/`$ref` so a shared object stays one object, while the default duplicates it. Jackson's managed/back reference pair drops the child-to-parent link entirely, which is itself a direction-of-ownership statement. ## When the layers disagree The recurring bugs are specific. An ORM cascade plus a database constraint that says something different — bulk SQL then bypasses the application rule and skips audit hooks. Orphan removal enabled on a genuinely shared part — detaching it from one whole destroys data the other still points at. An aggregator disposing a part it borrowed — the other whole gets a closed handle, invisible until a pooled resource is reused. Embed-on-write of a shared part — silent duplication on read. ## Choosing an authority Make the persistence mapping authoritative, because it is the layer that actually destroys data, and derive the rest from it: whoever owns the row owns disposal, and the wire embeds exactly what the mapping owns. Keep a database-level constraint only if it is identical to the mapping's rule, or deliberately weaker — a `RESTRICT`/`PROTECT` net that refuses rather than deletes is safer than a second, subtly different cascade. Note the asymmetry: under-claiming ownership leaves orphan rows and leaked handles, which is recoverable; over-claiming deletes somebody else's data, which is not. So before enabling any cascade, ask whether this part can legitimately belong to a second whole — if it can, no layer may delete it.
- The ORM cascades deletes and the database also declares ON DELETE CASCADE for the same relationship. Belt and braces, or a hazard?A hazard as soon as they differ, and they easily do. Application-level cascades run callbacks, signals and audit hooks the database knows nothing about, while database cascades also fire for bulk SQL and admin scripts that never touch the ORM. Pick one as authoritative; if you keep the constraint as a net, make it behave identically or make it refuse (RESTRICT/PROTECT) rather than delete.
- A method receives an open stream. How should it know whether it is responsible for closing it?From an explicit convention, not from guesswork. Either the caller who opened it closes it and the callee never does, or ownership transfer is stated in the signature — .NET's `leaveOpen` flag is the clearest form of this. Wrappers that unconditionally close what they wrap, as Java's decorators do, force you to insert a non-closing shim when the stream is shared, and closing a borrowed handle twice surfaces much later as a use-after-close on a pooled resource.
- The same part is read by two aggregates. Embed it or reference it?Reference it. Embedding declares ownership, and a part with two wholes has none, so embedding forks it into copies that drift apart independently. Embed only when the part has no identity outside its whole and is never read on its own; then embedding also removes a join and a class of partial-write anomalies.
The deed says who owns the house, the keyring says who can lock it, and the catalogue listing says whether it is sold with the furniture. Three documents about one property; only the deed decides what happens when it is demolished.
saying these in an interview costs you the question
- Believing `cascade = ALL` alone implements composition in JPA — without `orphanRemoval`, a child removed from the collection just becomes an orphan row.
- Assuming an ORM's cascade and the database's ON DELETE CASCADE are interchangeable; Django, for instance, cascades in Python and emits no such clause at all.
- Turning on orphan removal to make a diagram true, and thereby deleting a part another whole still references.
- Thinking garbage collection makes ownership irrelevant — handles, sockets and pooled connections still need exactly one closer.
- Treating embed-versus-reference as a payload-size decision only, then being surprised by duplicated parts after a round trip.