Progressive System Design
DeQL is designed for systems that grow and change. You don’t need to get everything right upfront. Start with the simplest possible model, validate it through inspection, and refine incrementally as understanding improves. The DeReg (Decision Registry) accepts new definitions dynamically, without requiring full system rebuilds or coordinated restarts.
The Problem with Big-Bang Design
Section titled “The Problem with Big-Bang Design”Traditional event-sourced systems often require:
- Define all aggregates and their invariants perfectly
- Design the complete event schema
- Wire up all command handlers
- Build projections for every read model
- Deploy the whole thing at once
This creates a high barrier to entry and makes iteration expensive.
DeQL’s Approach: Start Small, Grow Safely
Section titled “DeQL’s Approach: Start Small, Grow Safely”Stage 1 — Minimal Viable Decision
Section titled “Stage 1 — Minimal Viable Decision”Start with a single aggregate, command, event, and decision:
CREATE AGGREGATE Employee;
CREATE COMMAND HireEmployee (employee_id UUID, name STRING, grade STRING);
CREATE EVENT EmployeeHired (name STRING, grade STRING);
CREATE DECISION HireFOR EmployeeON COMMAND HireEmployeeEMIT AS SELECT EVENT EmployeeHired ( name := :name, grade := :grade );Inspect it immediately:
CREATE TABLE test_hires AS VALUES ('EMP-100', 'Charlie', 'L3'), ('EMP-101', 'Diana', 'L5');
INSPECT DECISION HireFROM test_hiresINTO simulated_hire_events;
SELECT stream_id, event_type, dataFROM simulated_hire_events;Or execute it live:
EXECUTE HireEmployee(employee_id := 'EMP-001', name := 'Alice', grade := 'L5');Stage 2 — Add More Behavior
Section titled “Stage 2 — Add More Behavior”As requirements become clearer, add new commands, events, and decisions:
CREATE COMMAND PromoteEmployee (employee_id UUID, new_grade STRING);
CREATE EVENT EmployeePromoted (new_grade STRING);
CREATE DECISION PromoteFOR EmployeeON COMMAND PromoteEmployeeEMIT AS SELECT EVENT EmployeePromoted ( new_grade := :new_grade );Add a second domain with state-dependent logic:
CREATE AGGREGATE BankAccount;
CREATE COMMAND OpenAccount (account_id UUID, initial_balance DECIMAL(12,2));CREATE COMMAND Deposit (account_id UUID, amount DECIMAL(12,2));
CREATE EVENT AccountOpened (initial_balance DECIMAL(12,2));CREATE EVENT Deposited (amount DECIMAL(12,2));
CREATE DECISION OpenFOR BankAccountON COMMAND OpenAccountEMIT AS SELECT EVENT AccountOpened ( initial_balance := :initial_balance );
CREATE DECISION DepositFundsFOR BankAccountON COMMAND DepositSTATE AS SELECT initial_balance AS balance FROM DeReg."BankAccount$Agg" WHERE aggregate_id = :account_idEMIT AS SELECT EVENT Deposited ( amount := :amount ) WHERE balance >= :amount;Stage 3 — Add Read Models
Section titled “Stage 3 — Add Read Models”Once the write side is stable, add projections for queries:
CREATE PROJECTION EmployeeRoster ASSELECT stream_id AS employee_id, LAST(data.name) AS name, LAST(data.grade) AS current_grade, LAST(data.new_grade) AS promoted_gradeFROM DeReg."Employee$Events"GROUP BY stream_id;
CREATE PROJECTION AccountBalance ASSELECT stream_id AS aggregate_id, SUM( CASE WHEN event_type = 'AccountOpened' THEN data.initial_balance WHEN event_type = 'Deposited' THEN data.amount ELSE 0 END ) AS balanceFROM DeReg."BankAccount$Events"GROUP BY stream_id;Stage 4 — Evolve Definitions Safely
Section titled “Stage 4 — Evolve Definitions Safely”Use CREATE OR REPLACE to overwrite existing definitions when schemas evolve:
CREATE OR REPLACE AGGREGATE BankAccount;CREATE OR REPLACE COMMAND OpenAccount (account_id UUID, initial_balance DECIMAL(12,2));CREATE OR REPLACE EVENT AccountOpened (initial_balance DECIMAL(12,2));Old events remain valid. Projections can handle both old and new event versions by filtering on event_type.
Evolution Patterns
Section titled “Evolution Patterns”Adding a New Guard
Section titled “Adding a New Guard”-- Before: any deposit acceptedCREATE DECISION DepositFundsFOR BankAccountON COMMAND DepositSTATE AS SELECT initial_balance AS balance FROM DeReg."BankAccount$Agg" WHERE aggregate_id = :account_idEMIT AS SELECT EVENT Deposited ( amount := :amount );
-- After: reject when balance is insufficientCREATE DECISION DepositFundsFOR BankAccountON COMMAND DepositSTATE AS SELECT initial_balance AS balance FROM DeReg."BankAccount$Agg" WHERE aggregate_id = :account_idEMIT AS SELECT EVENT Deposited ( amount := :amount ) WHERE balance >= :amount;Existing events are unaffected. The new guard only applies to future commands.
Adding Cross-Cutting Projections
Section titled “Adding Cross-Cutting Projections”-- Add audit trail without changing existing decisionsCREATE PROJECTION AuditLog ASSELECT stream_id, event_type, data, seqFROM DeReg."BankAccount$Events"ORDER BY seq DESC;Validating the Registry
Section titled “Validating the Registry”After making changes, validate the entire registry for consistency:
VALIDATE DEREG;And export the full system definition:
EXPORT DEREG;Key Principle
Section titled “Key Principle”Start with the simplest model that captures your core behavior. Inspect it. Ship it. Then evolve.
Every stage is a valid, working system. There is no “incomplete” intermediate state.