🗄️ Order Matching Engine — Database Schema & Relationships¶
Database: PostgreSQL 16 (post-trade / recovery store; not on the matching hot path) Purpose: Command journal for replay, book snapshots, order and trade projections for clients, clearing and audit Tables: 6 tables
The matching engine never reads from these tables while trading. The journal is the source of truth (in production it usually lives in a replicated log such as Aeron/Raft or Kafka and is archived here); orders and trades are projections rebuilt from events.
📊 Entity Relationship Diagram¶
erDiagram
instrument ||--o{ command_journal : "sequenced per symbol"
instrument ||--o{ book_snapshot : "periodic"
instrument ||--o{ orders : "trades in"
orders ||--o{ trades : "maker side"
orders ||--o{ trades : "taker side"
participant ||--o{ orders : "submits"
1. instrument¶
CREATE TABLE instrument (
symbol TEXT PRIMARY KEY,
tick_size NUMERIC(18, 8) NOT NULL CHECK (tick_size > 0),
lot_size BIGINT NOT NULL DEFAULT 1 CHECK (lot_size > 0),
partition_id INT NOT NULL, -- which engine shard owns it
status TEXT NOT NULL DEFAULT 'TRADING'
CHECK (status IN ('TRADING', 'HALTED', 'CLOSED'))
);
2. participant¶
CREATE TABLE participant (
participant_id TEXT PRIMARY KEY,
stp_group TEXT, -- self-trade prevention group
max_order_qty BIGINT NOT NULL,
kill_switch BOOLEAN NOT NULL DEFAULT FALSE
);
3. command_journal (source of truth)¶
CREATE TABLE command_journal (
symbol TEXT NOT NULL REFERENCES instrument(symbol),
seq BIGINT NOT NULL, -- assigned by the sequencer
session_date DATE NOT NULL,
command_type TEXT NOT NULL CHECK (command_type IN ('NEW', 'CANCEL', 'REPLACE')),
payload BYTEA NOT NULL, -- the serialized command, exactly as applied
sequenced_at TIMESTAMPTZ NOT NULL, -- stamped by the sequencer, part of the input
PRIMARY KEY (symbol, session_date, seq)
) PARTITION BY RANGE (session_date);
- Append-only; never updated. Replay =
SELECT ... WHERE symbol = $1 AND session_date = $2 AND seq > $3 ORDER BY seq. payloadstores the command as the engine saw it (ticks, not decimals) so replay doesn't depend on conversion code.
4. book_snapshot¶
CREATE TABLE book_snapshot (
symbol TEXT NOT NULL REFERENCES instrument(symbol),
session_date DATE NOT NULL,
last_seq BIGINT NOT NULL, -- replay resumes at last_seq + 1
payload BYTEA NOT NULL, -- every resting order in priority order
checksum TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (symbol, session_date, last_seq)
);
The snapshot must store resting orders in queue order per level, not just aggregated depth; otherwise time priority is lost on restore.
5. orders (projection)¶
CREATE TABLE orders (
symbol TEXT NOT NULL,
session_date DATE NOT NULL,
order_id BIGINT NOT NULL, -- = seq of the NewOrder command
participant_id TEXT NOT NULL REFERENCES participant(participant_id),
cl_ord_id TEXT NOT NULL, -- client's idempotency key
side TEXT NOT NULL CHECK (side IN ('BUY', 'SELL')),
order_type TEXT NOT NULL CHECK (order_type IN ('LIMIT', 'MARKET')),
tif TEXT NOT NULL CHECK (tif IN ('GTC', 'IOC', 'FOK')),
price_ticks BIGINT, -- NULL for market orders
quantity BIGINT NOT NULL CHECK (quantity > 0),
filled_qty BIGINT NOT NULL DEFAULT 0,
status TEXT NOT NULL,
last_seq BIGINT NOT NULL, -- last event applied; makes the projector idempotent
PRIMARY KEY (symbol, session_date, order_id),
UNIQUE (participant_id, session_date, cl_ord_id),
CHECK (filled_qty <= quantity)
);
CREATE INDEX idx_orders_open ON orders (participant_id)
WHERE status IN ('NEW', 'PARTIALLY_FILLED');
The projector applies an event only if event.seq > orders.last_seq, so redelivered events are no-ops.
6. trades¶
CREATE TABLE trades (
symbol TEXT NOT NULL,
session_date DATE NOT NULL,
trade_id BIGINT NOT NULL, -- deterministic per symbol/session
seq BIGINT NOT NULL, -- command that produced it
price_ticks BIGINT NOT NULL,
quantity BIGINT NOT NULL CHECK (quantity > 0),
maker_order_id BIGINT NOT NULL,
taker_order_id BIGINT NOT NULL,
taker_side TEXT NOT NULL,
PRIMARY KEY (symbol, session_date, trade_id)
);
CREATE INDEX idx_trades_maker ON trades (symbol, session_date, maker_order_id);
CREATE INDEX idx_trades_taker ON trades (symbol, session_date, taker_order_id);
trade_id is deterministic (a counter inside the book), so a replayed trade has the same primary key and INSERT ... ON CONFLICT DO NOTHING makes the projector idempotent.
🔑 Design notes¶
- Prices as
BIGINTticks, matching the engine. Convert to decimal (price_ticks * tick_size) only in views and reports. - Partition by
session_date: the journal is huge and only the current session is hot. Old partitions move to cheap storage and are dropped from the primary. - No foreign key from
tradestoorders: projections can arrive out of order across consumers, and the journal, not the FK, guarantees integrity. (participant_id, session_date, cl_ord_id)unique backs the gateway's idempotency check for status queries and resend handling.