Topic 12.4
Schema Patterns: Soft Delete, History, Temporal, Audit and Event Sourcing
In one line
Recurring schema patterns: soft delete (mark instead of remove), history and audit tables (who changed what, when), temporal tables with validity ranges (what was true at time T), snapshots, event sourcing (store events and derive state), plus the repository and unit-of-work patterns for application access. Each solves a real need and adds cost.
Think of it like this
A company's employee files. Leavers are marked "left" rather than shredded (soft delete), every salary change is logged with approver and date (audit), and HR can answer "what was her title on 1 March?" (temporal).
Key ideas
- 01
Soft delete:
deleted_at timestamptz. Every query must filter it (use views or row-level security), unique constraints need partial indexes (WHERE deleted_at IS NULL), and data protection laws may still require real deletion. Consider moving deleted rows to an archive table instead. - 02
History / audit tables:
order_history(order_id, version, changed_at, changed_by, before jsonb, after jsonb, request_id)written by triggers or the application in the same transaction. Append-only; restrict UPDATE and DELETE. - 03
Temporal (validity) modeling:
price(product_id, price, valid tsrange)with an exclusion constraint preventing overlaps; query "as of" withvalid @> timestamp. SQL:2011 system-versioned tables exist in MariaDB, SQL Server and Db2; PostgreSQL uses extensions or triggers. - 04
Event sourcing: store every state change as an immutable event (
AccountOpened,MoneyDeposited) and derive current state by replay, with snapshots for speed. It's great for audit-heavy domains and temporal queries; costly in schema evolution, projections and eventual consistency. The outbox is not event sourcing: it publishes events about state stored normally. - 05
Repository and unit of work: a repository hides persistence behind collection-like methods; a unit of work tracks changes and commits them in one transaction (JPA's persistence context is one). They keep domain code clean, but don't hide performance-relevant queries.
Code & diagrams
-- soft delete with a partial unique index
ALTER TABLE app_user ADD COLUMN deleted_at timestamptz;
CREATE UNIQUE INDEX app_user_email_live ON app_user (lower(email)) WHERE deleted_at IS NULL;
CREATE VIEW live_user AS SELECT * FROM app_user WHERE deleted_at IS NULL;
-- audit trigger (generic, JSONB before/after)
CREATE TABLE audit_log (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
table_name text NOT NULL, row_pk text NOT NULL, action text NOT NULL,
actor text NOT NULL DEFAULT current_setting('app.user_id', true),
request_id text DEFAULT current_setting('app.request_id', true),
at timestamptz NOT NULL DEFAULT now(), before jsonb, after jsonb
);
CREATE FUNCTION audit() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO audit_log (table_name, row_pk, action, before, after)
VALUES (TG_TABLE_NAME, coalesce(NEW.id, OLD.id)::text, TG_OP,
CASE WHEN TG_OP <> 'INSERT' THEN to_jsonb(OLD) END,
CASE WHEN TG_OP <> 'DELETE' THEN to_jsonb(NEW) END);
RETURN coalesce(NEW, OLD);
END $$;
CREATE TRIGGER orders_audit AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION audit();
-- temporal prices without overlaps
CREATE TABLE product_price (
product_id bigint NOT NULL, price numeric(10,2) NOT NULL, valid tstzrange NOT NULL,
EXCLUDE USING gist (product_id WITH =, valid WITH &&)
);
SELECT price FROM product_price WHERE product_id = 42 AND valid @> '2026-03-01'::timestamptz;Interview problem
The problem
Price history and audit for a regulated marketplace
Regulators require that you can show the price displayed for any product at any past time, who changed it, and why. Prices change a few thousand times per day. Design the schema.
When it breaks
Soft delete without query discipline
What you see
A report forgets deleted_at IS NULL and counts deleted users; a unique email constraint blocks a user from re-registering after deleting their account.
Fix & prevent
Expose views or RLS policies that hide deleted rows, and use partial unique indexes.
Explain it without notes
When is event sourcing worth its cost?
Practice
Query the audit log for everything request req-81 changed.
Trade-offs
- ↔
History and audit give accountability and time travel, at the cost of storage, write overhead and query discipline.
Done when you can
I can implement soft delete, audit, temporal and history patterns and judge event sourcing.