Skip to content

DEQL — A Declarative Way to Build CQRS & Event‑Sourced Systems

CQRS and Event Sourcing are powerful — DEQL removes their accidental complexity. Define commands, events, queries, and projections explicitly — without framework lock‑in or boilerplate.

DEQL lets you model CQRS‑ES systems as specifications, not scattered implementation details.

✅ Executable decisions
✅ First‑class inspection of events and aggregates
✅ Disposable, replayable projections

Without DeQL

  • Aggregates, commands, events scattered across files and frameworks
  • Business rules buried in imperative code
  • Projections hand-wired with custom event handlers
  • No built-in way to inspect or simulate decisions
  • Changing a rule means touching multiple layers
  • Testing requires spinning up infrastructure

With DeQL

  • Aggregates, commands, events declared in one place
  • Business rules expressed as guarded decisions (WHERE balance >= :amount)
  • Projections auto-generated from aggregate definitions
  • INSPECT lets you simulate decisions and projections without side effects
  • Changing a rule means editing one declaration
  • Testing is just INSPECT — no infrastructure needed

Let’s build a real CQRS-ES system from scratch. We’ll model an Employee domain that handles hiring and promotions.

First, define the aggregate boundary and the commands it accepts:

CREATE AGGREGATE Employee;
CREATE COMMAND HireEmployee (
employee_id STRING,
name STRING,
grade STRING
);
CREATE COMMAND PromoteEmployee (
employee_id STRING,
new_grade STRING
);

Register the events these commands can produce:

CREATE EVENT EmployeeHired (
name STRING,
grade STRING
);
CREATE EVENT EmployeePromoted (
new_grade STRING
);

Now wire up the business logic as decisions. Hiring is unconditional, but promotions are guarded — you can’t promote someone to the same grade:

CREATE DECISION Hire
FOR Employee
ON COMMAND HireEmployee
EMIT AS
SELECT EVENT EmployeeHired (
name := :name,
grade := :grade
);
CREATE DECISION Promote
FOR Employee
ON COMMAND PromoteEmployee
STATE AS
SELECT
LAST(
CASE
WHEN event_type = 'EmployeeHired' THEN data.grade
WHEN event_type = 'EmployeePromoted' THEN data.new_grade
ELSE NULL
END
) AS current_grade
FROM DeReg."Employee$Events"
WHERE stream_id = :employee_id
EMIT AS
SELECT EVENT EmployeePromoted (
new_grade := :new_grade
)
WHERE :new_grade <> current_grade;

Add read-side projections — a new-hire report and a promotions report:

CREATE PROJECTION NewHireReport AS
SELECT
stream_id AS employee_id,
LAST(data.name) AS name,
LAST(data.grade) AS hired_grade
FROM DeReg."Employee$Events"
WHERE event_type = 'EmployeeHired'
GROUP BY stream_id;
CREATE PROJECTION PromotionsReport AS
SELECT
stream_id AS employee_id,
seq,
data.new_grade AS promoted_to
FROM DeReg."Employee$Events"
WHERE event_type = 'EmployeePromoted'
ORDER BY employee_id, seq;

The system is ready. Execute some commands:

EXECUTE HireEmployee(employee_id := 'EMP-001', name := 'Alice', grade := 'L4');
EmployeeHired
stream_id: EMP-001
seq: 1
name: Alice
grade: L4
EXECUTE PromoteEmployee(employee_id := 'EMP-001', new_grade := 'L5');
EmployeePromoted
stream_id: EMP-001
seq: 2
new_grade: L5

Try promoting to the same grade again — the decision guard rejects it:

EXECUTE PromoteEmployee(employee_id := 'EMP-001', new_grade := 'L5');
REJECTED
decision: Promote
guard: :new_grade <> current_grade
state: current_grade = 'L5'
command: employee_id = 'EMP-001'
command: new_grade = 'L5'

Query the projections:

SELECT * FROM DeReg."NewHireReport" ORDER BY employee_id;
+-------------+-------+-------------+
| employee_id | name | hired_grade |
+-------------+-------+-------------+
| EMP-001 | Alice | L4 |
+-------------+-------+-------------+
SELECT * FROM DeReg."PromotionsReport";
+-------------+-----+-------------+
| employee_id | seq | promoted_to |
+-------------+-----+-------------+
| EMP-001 | 2 | L5 |
+-------------+-----+-------------+

That’s a full CQRS-ES system — aggregate, commands, events, guarded decisions, and projections — all declared, no boilerplate.


Want a simpler, repeatable process? Look at templates.

One template. One line. A fully operational event-sourced system.

APPLY TEMPLATE wallet_aggregate
WITH (wallet_name = 'Main', currency = 'USD');

That single line expands into a fully functional system:

  • AggregateMainWallet with typed state (wallet_id, currency, balance)
  • CommandsTopUpMain and DebitMain expressing caller intent
  • EventsMainWalletToppedUp and MainWalletDebited as immutable facts
  • DecisionsTopUpMain (unconditional credit) and DebitMain (guarded: WHERE balance >= :amount)
  • Default ProjectionMainWalletBalance read model auto-generated with the same fields as the aggregate

Spin up more wallets in one line each:

APPLY TEMPLATE wallet_aggregate
WITH (wallet_name = 'Promo', currency = 'USD');
APPLY TEMPLATE wallet_aggregate
WITH (wallet_name = 'Roaming', currency = 'USD');
APPLY TEMPLATE wallet_aggregate
WITH (wallet_name = 'CorporatePool', currency = 'USD');

Four aggregates, eight commands, eight events, eight decisions — zero boilerplate.

Send a command:

EXECUTE TopUpMain(wallet_id := 'wal-001', amount := 100.00);
MainWalletToppedUp
stream_id: wal-001
seq: 1
amount: 100.00
balance_after: 100.00

Query the projection:

SELECT * FROM DeReg.MainWalletBalance;
wallet_id | currency | balance
----------|----------|--------
wal-001 | USD | 100.00

Inspect before you ship:

CREATE TABLE test_topups AS
VALUES ('wal-001'::UUID, 100.00);
INSPECT DECISION TopUpMain
FROM test_topups
INTO simulated_events;
INSPECT PROJECTION MainWalletBalance
FROM simulated_events
INTO simulated_balances;
SELECT * FROM simulated_balances;

Inspection runs in production or any environment without altering domain facts.

Overview

Learn what DeQL is and the core philosophy behind it.

Language Reference

Explore the full language reference — aggregates, commands, events, decisions, projections, templates, and more.

Two-Phase Model

Understand the two-phase model — definitions then decision assembly.