Framework Query Shape
The SQL event-store adapters use these hot paths:| Operation | Query shape | Required access path |
|---|---|---|
| Load one aggregate stream | WHERE aggregate_type = ? AND aggregate_id = ? ORDER BY revision ASC | UNIQUE (aggregate_type, aggregate_id, revision) |
| Check current stream revision | MAX(revision) for one aggregate_type and aggregate_id | Same stream uniqueness index |
| Replay new global events for one aggregate type | WHERE aggregate_type = ? AND sequence > ? ORDER BY sequence ASC | INDEX (aggregate_type, sequence) |
| Load latest global rows for a small ledger view | ORDER BY sequence DESC LIMIT n | Primary key on sequence |
| Load or save checkpoints | projection_name point lookup/upsert | Primary key on projection_name |
| Load or save idempotency state | idempotency_key point lookup/upsert | Primary key on idempotency_key |
| Load or save snapshots | (aggregate_type, aggregate_id) point lookup/upsert | Primary key on (aggregate_type, aggregate_id) |
(aggregate_type, sequence) because projection replay is aggregate-type scoped. The stream
uniqueness constraint already covers stream loads, so a second identical stream
index is unnecessary in fresh schemas. Schema migration v6 removes the old
duplicate {events_table}_stream_idx index when it exists:
- SQLite and PostgreSQL drop
{events_table}_stream_idxdirectly withDROP INDEX IF EXISTS. - MySQL discovers non-unique indexes whose ordered columns are exactly
(aggregate_type, aggregate_id, revision)and drops only those duplicates, preserving the unique stream constraint and the global replay index.
Bounded Projection Replay
EventStore::load_global_after and projection runner run(...) methods remain
available for compatibility, but they load the full backlog after the
checkpoint. Production workers should prefer the bounded APIs:
500. SQL adapters apply LIMIT, Redis uses
ZRANGEBYSCORE ... LIMIT, and the in-memory store uses iterator take, so the
bounded path avoids fetching an unbounded tail in production adapters.
Read Models Own Product Queries
Do not turn the event table into an ad hoc reporting database. Avoid these patterns on hot paths:- Filtering or sorting by fields inside
payloadJSON. - Joining the event table directly into product screens.
- Replaying a full stream on every read request once the stream can grow large.
- Loading all global events just to show a small read-model value.
payload JSON to discover the current balance.
Checkpoints Must Move Forward
Projection checkpoints represent the last durable sequence a projection finished processing. Saving an older checkpoint can make a worker replay work it already completed. Use monotonic upserts:Eventual Consistency
Eventual consistency is a tradeoff, not a defect and not a magic guarantee. Advantages:- Command transactions stay small: validate aggregate state, append events, and return.
- Read models can be rebuilt, scaled, denormalized, and indexed for each screen.
- Slow reporting queries do not block command writes.
- A read model can lag behind the command response.
- Realtime notifications can be duplicated, delayed, or missed.
- UIs must avoid rewinding optimistic state when an older read-model snapshot arrives.
Realtime Is A Wake Signal
SSE, WebSocket, polling, and Redis pub/sub are transport choices. They do not replace durable event replay. The safe pattern is:- Command appends events to the durable store.
- Server publishes a wake notification.
- Client or worker loads durable events after its last known
sequence. - Client ignores duplicate or older sequences.
Query Review Checklist
Before adding a database query:- Identify whether it belongs to the write model, event replay, checkpointing, idempotency, snapshots, or a read model.
- Check the
WHEREandORDER BYcolumns against an existing primary key, unique constraint, or index. - Use a read model for product queries that filter by business fields.
- Keep projection catch-up bounded or run it outside the request path when backlogs can become large.
- Use
EXPLAINor the database query planner before claiming a query is optimized. - Update this guide and the counter-app docs when a new query pattern becomes part of the recommended workflow.
Verifying Plans
The test suite includes SQLite planner assertions by default when thesqlite
feature is enabled. PostgreSQL and MySQL plan tests are live-gated: