Schema design for B2B orders with per-customer price lists and validity ranges

Backend Live Coding November 13, 2026 sql

Points I explain out loud in the interview:

sql
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
-- ======================================================================
-- Q: Design a schema for B2B orders with per-customer price lists that are valid
-- from-to.
--
-- Points I explain out loud in the interview:
-- - EXCLUDE with daterange: the database forbids two price lists that overlap in
--   time for the same customer. With a check in code there would be a race
--   condition between two concurrent admins.
-- - Partial unique index: one draft cart per customer, but unlimited completed
--   orders. Same for the ERP external_ref, which makes the sync idempotent
--   (INSERT ... ON CONFLICT).
-- - Snapshot of price and VAT on the line: if the price list changes, the
--   invoice does not change.
-- - ON DELETE RESTRICT for customers with history, CASCADE only for "children"
--   that make no sense on their own.
-- - In Django: ExclusionConstraint (django.contrib.postgres),
--   UniqueConstraint(condition=Q(...)), CheckConstraint.
-- ======================================================================

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE customers (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    vat_number  varchar(20) NOT NULL UNIQUE,
    name        text        NOT NULL,
    kind        text        NOT NULL CHECK (kind IN ('b2b', 'b2c')),
    deleted_at  timestamptz
);

CREATE TABLE products (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sku         varchar(40) NOT NULL UNIQUE,
    name        text        NOT NULL,
    vat_rate    numeric(4,2) NOT NULL CHECK (vat_rate >= 0)
);

CREATE TABLE price_lists (
    id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id bigint NOT NULL REFERENCES customers(id) ON DELETE RESTRICT,
    currency    char(3) NOT NULL DEFAULT 'EUR',
    valid       daterange NOT NULL,
    -- overlapping price lists for the same customer are not allowed
    EXCLUDE USING gist (customer_id WITH =, valid WITH &&)
);

CREATE TABLE price_list_items (
    price_list_id bigint NOT NULL REFERENCES price_lists(id) ON DELETE CASCADE,
    product_id    bigint NOT NULL REFERENCES products(id),
    unit_cents    integer NOT NULL CHECK (unit_cents >= 0),
    PRIMARY KEY (price_list_id, product_id)
);

CREATE TABLE orders (
    id           bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    customer_id  bigint NOT NULL REFERENCES customers(id),
    status       text   NOT NULL DEFAULT 'draft'
                 CHECK (status IN ('draft','placed','paid','shipped','cancelled')),
    external_ref varchar(64),               -- ERP code
    total_cents  bigint NOT NULL DEFAULT 0 CHECK (total_cents >= 0),
    created_at   timestamptz NOT NULL DEFAULT now()
);
-- only one open cart (draft) per customer: partial unique index
CREATE UNIQUE INDEX ux_orders_one_draft ON orders (customer_id) WHERE status = 'draft';
CREATE UNIQUE INDEX ux_orders_external_ref ON orders (external_ref)
    WHERE external_ref IS NOT NULL;
CREATE INDEX ix_orders_cust_created ON orders (customer_id, created_at DESC);

CREATE TABLE order_lines (
    order_id    bigint  NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
    line_no     smallint NOT NULL,
    product_id  bigint  NOT NULL REFERENCES products(id),
    qty         integer NOT NULL CHECK (qty > 0),
    unit_cents  integer NOT NULL,           -- snapshot of the price at the time of the order
    vat_rate    numeric(4,2) NOT NULL,      -- snapshot of VAT
    PRIMARY KEY (order_id, line_no)
);