Skip to content

AGGREGATE

In DeQL, an Aggregate defines an event‑sourced state boundary. It represents a logical boundary around a group of events and the state derived from them.

Aggregates are declared as named entities. Their current derived state is queried at decision time via the $Agg relation.

Aggregates serve as the state input to decisions. When a decision evaluates whether a command is valid, it derives the current aggregate state using a STATE AS SELECT ... FROM DeReg."<Aggregate>$Agg".

Unlike traditional DDD aggregates that encapsulate both state and behavior, a DeQL aggregate is:

  • Derived — built entirely from events
  • Deterministic — the same event stream always produces the same state
  • Disposable — can be rebuilt from scratch at any time
  • Passive — contains no business logic

An aggregate defines an identity and consistency boundary, not a data structure.

CREATE AGGREGATE <Name>;

Use CREATE OR REPLACE to overwrite an existing definition:

CREATE OR REPLACE AGGREGATE <Name>;

The aggregate is a named container. Its state shape is implicitly defined by the events it receives and the projections/decisions that query it.

CREATE AGGREGATE Employee;
CREATE AGGREGATE BankAccount;

Additional examples:

CREATE AGGREGATE ShoppingCart;
CREATE AGGREGATE Inventory;
CREATE AGGREGATE Subscription;

Aggregate state is accessed inside decisions using the $Agg suffix with quoted identifiers and SQL-like SELECT:

CREATE DECISION DepositFunds
FOR BankAccount
ON COMMAND Deposit
STATE AS
SELECT initial_balance AS balance
FROM DeReg."BankAccount$Agg"
WHERE aggregate_id = :account_id
EMIT AS
SELECT EVENT Deposited (
amount := :amount
)
WHERE balance >= :amount;

The DeReg."BankAccount$Agg" reference gives you the current derived state of the aggregate, filtered by aggregate_id.

Events emitted against an aggregate automatically contribute to its derived state. The aggregate’s $Agg view is the result of folding all events for a given aggregate_id in order.

-- These events shape BankAccount's state
CREATE EVENT AccountOpened (
initial_balance DECIMAL(12,2)
);
CREATE EVENT Deposited (
amount DECIMAL(12,2)
);
-- These events shape Employee's state
CREATE EVENT EmployeeHired (
name STRING,
grade STRING
);
CREATE EVENT EmployeePromoted (
new_grade STRING
);

When a decision queries DeReg."BankAccount$Agg", it sees the latest state derived from all AccountOpened and Deposited events for that aggregate instance.

You can query aggregate state directly:

-- All BankAccount aggregate state
SELECT * FROM DeReg."BankAccount$Agg";
-- Filter by aggregate_id
SELECT * FROM DeReg."BankAccount$Agg" WHERE aggregate_id = 'ACC-001';
-- Select specific columns
SELECT aggregate_id, initial_balance FROM DeReg."BankAccount$Agg";
-- All Employee aggregate state
SELECT * FROM DeReg."Employee$Agg";

You can also query the raw event stream using the $Events suffix:

-- Query the event stream directly
SELECT stream_id, event_type, seq, data
FROM DeReg."BankAccount$Events"
ORDER BY stream_id, seq;
-- Build a projection from the event stream
CREATE PROJECTION AccountBalance AS
SELECT
stream_id AS aggregate_id,
LAST(data.initial_balance) AS balance
FROM DeReg."BankAccount$Events"
GROUP BY stream_id;
SuffixPurposeExample
$AggCurrent derived state of the aggregateDeReg."BankAccount$Agg"
$EventsRaw event stream for the aggregateDeReg."BankAccount$Events"

Both are derived from events, but they serve different roles:

AspectAggregateProjection
PurposeDecision input (write side)Query output (read side)
ScopeSingle entity by aggregate_idCross-entity, denormalized
Access$Agg in STATE AS clauseStandalone query
LifecycleRebuilt per decision evaluationMaintained continuously
DisposabilityRebuilt on every decisionRebuilt on demand

Aggregates are independent state boundaries, but decisions are not limited to a single aggregate. A decision’s STATE AS clause can join multiple $Agg providers to read state from several aggregates in one query:

STATE AS
SELECT
c.applied_coupon,
q.quantity AS coupon_quantity
FROM DeReg."ShoppingCart$Agg" c
JOIN DeReg."Coupon$Agg" q ON q.aggregate_id = :coupon_id
WHERE c.aggregate_id = :user_id

This avoids the need for sagas or process managers when a business rule spans multiple aggregates. See the Decision docs for full examples.