Skip to content

Employee Domain — Getting Started

A step-by-step introduction to DeQL using an Employee domain. Covers every core concept: aggregate boundaries, commands, events, guarded decisions, and projections.

An HR system that handles hiring and promotions. An employee can be hired unconditionally, but promotions are guarded — you can’t promote someone to the same grade they already hold.

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

Hiring is unconditional — any valid command produces an event:

CREATE DECISION Hire
FOR Employee
ON COMMAND HireEmployee
EMIT AS
SELECT EVENT EmployeeHired (
name := :name,
grade := :grade
);

Promotion is guarded. The decision derives the current grade from event history and only emits if the new grade differs:

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;

Two read models from the same event stream — one for Finance, one for Accounts:

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;
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 — the 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'
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 |
+-------------+-----+-------------+
  • Aggregate as a consistency boundary
  • Commands expressing intent
  • Events as immutable facts
  • Guarded decisions with STATE AS + WHERE
  • Projections as derived read models
  • Rejection with full diagnostic output (guard, state, command values)