skip to content

A row's type marker names a subclass the running build does not contain. What happens, and how do you handle it?

level: seniorimportance: should knowfreq 36%

answer

  1. marker resolves through a class registry
  2. no entry means a failed read
  3. base-type queries select every marker
  4. reader before writer on a rolling deploy
  5. marker set behaves like a contract

basics

~20 s

Hydration cannot resolve the marker to a class, so the read fails, usually the whole query rather than the row, and it surfaces on base-type queries that select every marker. Fix the reader first, then the rows.

solid answer

~50 s

Hydration turns a marker value into a class by looking it up in a registry built from the mapping metadata. An unregistered value has no entry, so most layers raise an error, and because results materialise together it typically takes down the whole read rather than one row. The failure shows up on **base-type** queries, which select every marker, so it hits general list screens and background sweeps rather than the feature that introduced the subtype. Usual causes are a rolling deploy or rollback where writer and reader disagree, a class deleted or renamed while its rows remain, or a second writer inventing values this build never registered. The order of recovery matters: count the distinct markers actually present, deploy the build that knows the value, and only then migrate or retire rows. Prevention is explicit stable codes, reader-before-writer deploys, and treating the marker set as append-only.

go deeper

for a junior

Know that the marker value is looked up against the classes the build knows, and that an unknown value is an error rather than a skipped row.

for a middle

Explain why base-type queries are the ones that fail, and how a rolling deploy puts a writer ahead of a reader.

for a senior

Show the recovery order — measure the distinct markers, restore the reader, then migrate rows — and name the prevention: stable codes, reader-first deploys, append-only marker values.

for a principal

Treat the marker set as a published contract across every reader of that table, including other services, and set the release discipline that keeps it that way.

## What actually happens Hydration of a row in a mapped hierarchy is a lookup: take the marker value, find the class registered for it, instantiate. When the registry has no entry — because the class is absent from this build, was renamed, was moved, or was never deployed here — the lookup has nothing to return. Almost every layer treats that as an error rather than as data to skip, and the error surfaces at read time, in whatever code was innocently running a query. Two properties make it worse than a single bad row: - **It usually fails the whole read, not the row.** Results are materialised together, so one unresolvable marker can abort a page of a list rather than degrade it. - **It is triggered by base-type queries.** A query for the concrete types this build knows about never sees the offending rows; a query for the base type selects all markers and therefore selects the unknown one. The failure appears on general list screens and background sweeps, not on the feature that introduced the marker. ## Common ways to arrive here - A **rolling deploy** where the new build writes rows with a new marker while older instances are still serving reads. - A **rollback** to a build that predates the subtype, with the new rows still in the table. - **Deleting a subtype** from the code without migrating or retiring its rows. - **Renaming or moving the class** when the marker value is derived from the class name rather than declared explicitly. - **Another writer** — a second service, an import job, a fixture — writing marker values that the reading build's metadata never registered. ## Handling it when it is already happening 1. **Establish which markers exist in the data**, not in the code: a grouped count over the marker column is one cheap query and it tells you scope immediately. 2. **Restore the reader before you touch data.** Deploying the build that knows the marker is usually faster and safer than rewriting rows under a running system. 3. **Only then decide about the rows**: migrate them to a marker the model keeps, or retire them. Rewriting markers is a data change and deserves the same care as any other. 4. **If reads must survive right now** and the layer supports a fallback mapping for unrecognised markers, that buys time — but a fallback hydrates a base-typed object with the subtype's fields unavailable, so it is a stopgap, not a design. 5. Where the layer has no fallback, narrowing the failing query to the concrete types this build knows keeps the screen alive while the deploy catches up. ## Preventing it | practice | what it buys | |---|---| | Declare explicit, stable marker codes | class renames and package moves stop being data migrations | | Deploy the reader before the writer | no instance can meet a marker it cannot resolve | | Treat marker values as append-only | old rows stay readable; retirement becomes a deliberate migration | | Keep one owner for the table's markers | a second writer cannot invent values the reader lacks | | Assert the code's registry against the data's distinct markers | drift is caught by a test or a check, not by a user | ## The judgment underneath A type marker turns part of the class model into stored data, and stored data has a longer life and a wider audience than the build that wrote it. That is why the marker set behaves like a published contract: values are added carefully, retired deliberately, and never repurposed. Deployment order matters for the same reason a schema change's order matters — during a rolling release, two builds read the same table, and the one that knows fewer markers sets the limit on what may be written. Teams that internalise this treat "add a subtype" as a three-step change — register the marker everywhere, then start writing it, then use it — rather than a single commit. The same reasoning applies in reverse when a subtype is retired. The class can only be deleted once no row still carries its marker, so the retirement is a data task with a code task attached, not the other way round: stop writing the value, migrate or archive the rows that hold it, confirm the count is zero, and only then remove the class and its registration. Skipping the confirmation is how a build ships that cannot read rows it wrote last month, and the failure will not appear until something runs a base-type query over old data — typically an export or a report, long after the release that caused it.

  • Why does this surface on list screens rather than on the feature that added the subtype?
    That feature queries its own concrete type, which never selects the unknown marker. A base-type query selects all markers, so it is the general list, the export, or the nightly sweep that meets the unresolvable row first — often long after the deploy that caused it.
  • If the layer supports a fallback mapping for unrecognised markers, is that a fix?
    It is a stopgap. Reads survive, but the row hydrates as a base-typed object with the subtype's own fields unavailable, so any logic that depends on the concrete type is silently wrong. Use it to keep a screen alive while the correct build rolls out, not as a design.
  • What deployment order avoids the problem entirely?
    Deploy the build that knows the new marker everywhere first, and only then enable the code path that writes rows with it. No instance can then meet a value it cannot resolve, which makes adding a subtype a three-step change: register the marker, start writing it, then use it.
  • How do you detect drift between the code's markers and the data's before users do?
    Compare the two directly: a grouped count over the marker column gives the distinct values in the data, and the mapping metadata gives the values the build registers. Asserting one against the other in a check or a startup validation turns a production read failure into a build or deploy signal.

saying these in an interview costs you the question

  • Expects the unresolvable row to be skipped and the rest returned
  • Assumes the layer falls back to the base class by default
  • Rewrites marker values in the table before fixing the reader
  • Treats marker values as safe to rename with the class
  • Thinks concrete-type queries would have caught it earlier