Skip to content
Vinod Kumar PeddiSoftware Engineer
Section 1 of 7: Intro
02CQRS · Event-driven architecture2026

Event-Driven Analytics

An e-commerce analytics backend that separates writes from reads with a transactional outbox and RabbitMQ.

  • Node.js
  • Express
  • amqplib
  • RabbitMQ 3
  • PostgreSQL 14 ×2
  • Docker Compose
CQRS · Event-driven architecture
Pattern
CQRS · transactional outbox · materialized views
Guarantees
At-least-once delivery · idempotent consumers · DLQ
Services
command :8080 · consumer · query :8081
01Overview

Overview

The system separates the write path (creating products and orders) from the read path (sales analytics) using CQRS. Each side has its own PostgreSQL database and its own service, connected by RabbitMQ.

Analytics queries hit pre-computed materialized views instead of joining across the transactional schema, keeping reads fast regardless of write volume.

02Problem

Problem

Analytics workloads and transactional workloads want different shapes of data. A normalised write model is good for consistency and terrible for aggregate queries; a denormalised read model is the reverse.

Keeping the two in sync asynchronously introduces the dual-write problem: a service that writes to its database and then publishes to a broker can crash between the two and lose the event forever.

03Architecture

Architecture

  1. 01Command Service (:8080) exposes POST /api/products and POST /api/orders. Each command writes business rows and an outbox row in one transaction.
  2. 02An outbox publisher relays unpublished outbox rows to RabbitMQ, guaranteeing at-least-once delivery.
  3. 03Consumer Service subscribes to the exchange, updates the read database and records each event id in processed_events so redelivered messages are ignored. Failures route to a dead-letter queue.
  4. 04Query Service (:8081) serves analytics such as GET /api/analytics/products/{id}/sales from materialized views.
04Implementation

Implementation

Command with outbox

The business write and the outbox insert are wrapped in a single transaction; if either fails, neither is committed.

Shape of a command transaction
1BEGIN;2INSERT INTO orders (customer_id, total) VALUES ($1, $2) RETURNING id;3INSERT INTO order_items (order_id, product_id, quantity, price) VALUES ...;4INSERT INTO outbox (aggregate_id, event_type, payload)5  VALUES ($order_id, 'OrderCreated', $payload);6COMMIT;

Idempotent consumption

Because delivery is at-least-once, the consumer must tolerate duplicates. Each processed event id is stored; a repeat is acknowledged and skipped.

Materialized read models

The read database holds pre-aggregated per-product sales figures, refreshed by the consumer as events arrive, so the query service does no heavy joins at request time.

05Challenges

Challenges

01

Losing events between DB and broker

Solved by the outbox: the event is durable the moment the transaction commits, and the relay can be retried safely.

02

Duplicate deliveries

At-least-once implies duplicates. The processed_events table makes the consumer idempotent.

03

Poison messages

Malformed or repeatedly failing messages are routed to a DLQ for inspection instead of blocking the queue.

06Technical decisions

Technical decisions

01

Two databases, not two schemas

Physically separating the read and write stores makes the isolation explicit and lets each be tuned independently.

02

Outbox over dual writes

Simpler alternatives (publish after commit) are exactly the failure mode the pattern exists to remove.

03

RabbitMQ

A mature broker with acknowledgements, durable queues and dead-lettering built in.

07Result

Result

A containerised CQRS system where writes stay normalised, reads stay fast, and the path between them survives crashes and redeliveries.

08Lessons

Lessons

  • 01Consistency in event-driven systems is a design decision made at the transaction boundary.
  • 02Idempotency and dead-lettering are not optional extras — they are what make at-least-once delivery usable.