Inspect Projection
A small e-commerce shop wants to validate that its reporting projections produce correct output before going live. The team uses INSPECT PROJECTION to verify the read models — without updating the real projections.
Domain
Section titled “Domain”An online shop with orders and cancellations. Two projections: revenue by product and orders per customer.
Define the System
Section titled “Define the System”CREATE AGGREGATE Shop;
CREATE COMMAND PlaceOrder ( order_id STRING, customer_id STRING, product STRING, quantity INT, unit_price DECIMAL);
CREATE COMMAND CancelOrder ( order_id STRING, reason STRING);
CREATE EVENT OrderPlaced ( customer_id STRING, product STRING, quantity INT, unit_price DECIMAL);
CREATE EVENT OrderCancelled ( reason STRING);
CREATE DECISION PlaceOrderFOR ShopON COMMAND PlaceOrderEMIT AS SELECT EVENT OrderPlaced ( customer_id := :customer_id, product := :product, quantity := :quantity, unit_price := :unit_price );
CREATE DECISION CancelOrderFOR ShopON COMMAND CancelOrderSTATE AS SELECT LAST(event_type) AS current_status FROM DeReg."Shop$Events" WHERE stream_id = :order_idEMIT AS SELECT EVENT OrderCancelled ( reason := :reason ) WHERE current_status = 'OrderPlaced';Define Projections
Section titled “Define Projections”Revenue by product:
CREATE PROJECTION RevenueByProduct ASSELECT data.product AS product, SUM(data.quantity) AS total_units, SUM(data.quantity * data.unit_price) AS total_revenueFROM DeReg."Shop$Events"WHERE event_type = 'OrderPlaced'GROUP BY data.product;Orders per customer:
CREATE PROJECTION CustomerOrders ASSELECT data.customer_id AS customer_id, COUNT(*) AS order_count, SUM(data.quantity * data.unit_price) AS total_spentFROM DeReg."Shop$Events"WHERE event_type = 'OrderPlaced'GROUP BY data.customer_id;Place Real Orders
Section titled “Place Real Orders”Build up some state in the event store:
EXECUTE PlaceOrder(order_id := 'ORD-001', customer_id := 'CUST-A', product := 'Widget', quantity := 3, unit_price := 25.00);
✓ OrderPlaced stream_id: ORD-001 seq: 1 customer_id: CUST-A product: Widget quantity: 3 unit_price: 25
EXECUTE PlaceOrder(order_id := 'ORD-002', customer_id := 'CUST-B', product := 'Gadget', quantity := 1, unit_price := 150.00);
✓ OrderPlaced stream_id: ORD-002 seq: 1 customer_id: CUST-B product: Gadget quantity: 1 unit_price: 150
EXECUTE PlaceOrder(order_id := 'ORD-003', customer_id := 'CUST-A', product := 'Widget', quantity := 2, unit_price := 25.00);
✓ OrderPlaced stream_id: ORD-003 seq: 1 customer_id: CUST-A product: Widget quantity: 2 unit_price: 25
EXECUTE PlaceOrder(order_id := 'ORD-004', customer_id := 'CUST-C', product := 'Gadget', quantity := 5, unit_price := 150.00);
✓ OrderPlaced stream_id: ORD-004 seq: 1 customer_id: CUST-C product: Gadget quantity: 5 unit_price: 150Query the real projections:
SELECT * FROM DeReg."RevenueByProduct";
+---------+-------------+---------------+| product | total_units | total_revenue |+---------+-------------+---------------+| Widget | 5 | 125.00 || Gadget | 6 | 900.00 |+---------+-------------+---------------+SELECT * FROM DeReg."CustomerOrders";
+-------------+-------------+-------------+| customer_id | order_count | total_spent |+-------------+-------------+-------------+| CUST-C | 1 | 750.00 || CUST-A | 2 | 125.00 || CUST-B | 1 | 150.00 |+-------------+-------------+-------------+INSPECT PROJECTION — Simulate the Read Model
Section titled “INSPECT PROJECTION — Simulate the Read Model”Now run the same projection logic against the event store, but write the output to a separate table instead of updating the real projection:
INSPECT PROJECTION RevenueByProductFROM DeReg."Shop$Events"INTO simulated_revenue;
INSPECT PROJECTION → 2 row(s) projected → simulated_revenue
SELECT * FROM simulated_revenue;
+---------+-------------+---------------+| product | total_units | total_revenue |+---------+-------------+---------------+| Widget | 5 | 125.00 || Gadget | 6 | 900.00 |+---------+-------------+---------------+INSPECT PROJECTION CustomerOrdersFROM DeReg."Shop$Events"INTO simulated_customers;
INSPECT PROJECTION → 3 row(s) projected → simulated_customers
SELECT * FROM simulated_customers;
+-------------+-------------+-------------+| customer_id | order_count | total_spent |+-------------+-------------+-------------+| CUST-C | 1 | 750.00 || CUST-B | 1 | 150.00 || CUST-A | 2 | 125.00 |+-------------+-------------+-------------+The simulated tables match the real projections. The real projections were not touched.
When to Use INSPECT PROJECTION
Section titled “When to Use INSPECT PROJECTION”| Scenario | Why |
|---|---|
| Validating projection logic changes | Test a modified projection query against real events before deploying |
| Shadow builds | Build a v2 projection alongside v1 — compare outputs, swap when ready |
| Regression testing | Verify projection output hasn’t changed after event schema evolution |
| Read model design | Prototype a new projection and verify its shape before registering it |
| Full rebuild | Replay all events through a projection into a fresh target table |
What This Demonstrates
Section titled “What This Demonstrates”- INSPECT PROJECTION runs projection logic against any event source without updating the real projection
- FROM DeReg.”Aggregate$Events” reads from the real event store
- INTO simulated_table writes to a separate output table
- Summary output —
N row(s) projectedgives instant feedback - Same logic, no side effects — the projection query is identical to production, but nothing is mutated