What error context must a quarantine table carry alongside each rejected record?
answer
- what arrived, why, from where
- never store the coerced version
- codes group, messages do not
- you must be able to find the original
- it needs an exit path too
basics
~20 sStore the payload exactly as it arrived, the check that rejected it with a stable reason code, where it came from (file and line, or topic, partition and offset), the pipeline run id, arrival and rejection timestamps, and a status field that drives replay.
solid answer
~50 sA rejected record is only useful if someone can answer three questions from the row alone: what arrived, why it was refused, and where it came from. So keep the **raw payload verbatim** — the pre-cast, pre-parsed bytes or string — because the coercion is usually what failed and a cleaned copy hides the evidence. Add a **stable machine-readable reason code** plus the human message and the name of the rule, so you can group by reason and replay only the class you fixed. Add a **source locator**: file name and line or byte offset for batch, topic/partition/offset or the source primary key for streams. Add the **run id and timestamps** so you can tie rejects to a deployment or a source change. Finally add **lifecycle fields** — status, attempt count, replayed-at — because a quarantine store without an exit path becomes a landfill.
code
sql · 18 linesCREATE TABLE orders_quarantine (
quarantine_id bigserial PRIMARY KEY,
raw_record text NOT NULL, -- exactly as it arrived, never coerced
reject_reason text NOT NULL, -- stable code, e.g. amount_negative
reject_message text, -- human detail
rule_name text NOT NULL,
source_file text, -- batch locator
source_line integer,
source_topic text, -- stream locator
source_partition integer,
source_offset bigint,
run_id text NOT NULL,
arrived_at timestamptz NOT NULL,
rejected_at timestamptz NOT NULL DEFAULT now(),
status text NOT NULL DEFAULT 'new',
attempt_count integer NOT NULL DEFAULT 0,
replay_run_id text
);go deeper
Remember the minimum: the record as it arrived, why it was rejected, and where it came from. Saying you would keep the raw payload rather than a cleaned copy already puts you ahead at this level.
Explain each field and what it is for — codes for grouping, locators for provenance, run id for correlating with a deployment. Be ready to say why the coerced value must not replace the raw one.
Demonstrate operational use: grouping reject rate by reason, spotting a new reason code, selecting a replay set by reason and time window, and alerting on the oldest untriaged record.
Own the policy side — retention, access control and deletion-request handling for a store that holds unmasked customer payloads, and the standard schema you would mandate so every pipeline's rejects are queryable the same way.
## Why the raw payload, verbatim The single most common mistake is storing the *parsed* version of a record that failed parsing. If a date column blew up on the string `31/02/2026`, a quarantine table typed `DATE` cannot hold it; if the pipeline coerced it to null before writing, the reject row now says the value was missing when in fact it was malformed. Store the record as it arrived — the raw line, the raw JSON document, the serialized bytes — in a text, JSON or binary column. That copy is the only artefact that lets a human reproduce the failure and the only input a replay can legitimately use. Where the record arrived as structured input, storing both the raw form and a best-effort parsed form is fine, but the raw form is the one that must never be lossy. ## Naming the failure so it can be grouped A free-text error message is enough to read one row and useless for operating a pipeline. What you need is a **stable reason code** — a short identifier like `null_order_id`, `amount_negative`, `unknown_currency`, `json_parse_error` — plus, separately, the human-readable message and the identifier of the rule or expectation that produced it. Codes matter because everything operational is an aggregate: reject rate *by reason*, the alert that fires when a new reason appears for the first time, the replay that selects only the rows whose reason matches the fix you just shipped. If your reject reasons are exception strings containing row values, none of that grouping works and cardinality explodes. Where a record fails several checks, decide deliberately: record only the first failure and you lose the rest; record all of them (an array of codes) and triage sees the full picture at the cost of a slightly richer schema. The richer version is usually worth it. ## The source locator "Where did this come from" has a different answer per ingestion style, and the quarantine schema should carry whichever applies: - **File-based batch**: source file name (including its dated prefix), line number or byte offset, and the batch or manifest id. - **Streaming**: the topic, partition and offset, plus the message key. - **Database extraction**: the source table and the primary key, plus the watermark value the extract was reading at. The test for whether the locator is good enough is blunt: could an engineer, given only this quarantine row, go back to the source and find the original? If not, the record's provenance is broken and you cannot prove to a producer that their system emitted it. ## Run and time metadata Record the **pipeline run id**, the **code or config version** that processed it, the **arrival timestamp** and the **rejection timestamp**. This is what turns quarantine into a diagnostic tool rather than a pile: rejects that all start at one run id point at a deployment; rejects that all start at one arrival timestamp point at a source change; rejects spread evenly across runs are ordinary data noise. Keep the record's own business or event timestamp too where it can be extracted, because replay ordering may depend on it. ## Lifecycle fields A quarantine record has a life: it arrives, it is triaged, and it either replays successfully or is closed as unloadable. Model that explicitly with a `status` column (`new`, `triaged`, `pending_replay`, `replayed`, `discarded`), an `attempt_count`, a `replay_run_id` and a `resolved_at`/`resolved_by`. Two things fall out for free: you can cap attempts so a record that keeps failing does not cycle forever, and you can alert on the age of the oldest record still in `new`, which is the metric that catches a quarantine store nobody is reading. ## What you should think twice about storing Rejected records are often the *most* sensitive rows you hold, precisely because they escaped the pipeline's normal masking, typing and access controls. A quarantine store frequently ends up with raw customer payloads sitting in a table with looser permissions than the target. Treat it as production data: same access control, an explicit retention period after which resolved records are purged, and inclusion in whatever deletion-request process applies to the target. If the raw payload cannot be retained under policy, store a redacted copy plus enough structure to diagnose, and accept that replay may then require re-extraction. ## The shape that works One wide table per pipeline (or one per source family) with the payload as JSON or text, the reason code and rule name, the locator columns, the run and time metadata, and the lifecycle columns. Partition it by rejection date so retention is a partition drop, and index it by reason code and status, because those are the two predicates every triage query and every replay query uses.
- Why is a stable reason code better than the exception message for the reject reason?Because every operational use is an aggregate. You alert on reject rate by reason, you notice a reason appearing for the first time, and you replay only the rows whose reason matches the fix you shipped. Exception strings embed row values, so cardinality explodes and grouping breaks. Keep the message as a secondary column.
- What are the privacy implications of keeping raw rejected payloads?Rejected rows bypassed the pipeline's masking and typing, so quarantine often holds the rawest customer data you have, sometimes under looser permissions than the target. Give it the same access control as production, set an explicit retention that purges resolved records, and include it in deletion-request handling. If policy forbids retaining the payload, store a redacted copy and accept that replay may need re-extraction.
- Should a record that fails several checks store one reason or all of them?Prefer all of them, as an array of codes. Storing only the first failure means a triage engineer fixes one defect, replays, and the record bounces back on the second — burning a cycle per problem. Recording every failing rule costs a little schema complexity and gives triage the whole picture in one pass.
saying these in an interview costs you the question
- Writing the parsed row into quarantine after parsing is what failed
- Using the raw exception text as the reject reason
- Omitting the file, offset or key so the original cannot be found
- Treating quarantine as append-only with no status or replay tracking
- Giving the quarantine store weaker access control than the target table