Low Level Design: CQRS Service

CQRS Service: Low Level Design

CQRS (Command Query Responsibility Segregation) separates the write model (commands) from the read model (queries), enabling independent scaling, optimization, and consistency strategies for each side.

Architecture Overview

The system splits into two distinct paths: the command side handles all state mutations, while the query side serves all read requests from a denormalized projection.

Command Side

Command Handler

The CommandHandler receives incoming commands, validates business rules against the write model, persists the command, and publishes a domain event to Kafka.

class CommandHandler:
    def handle(self, command: Command) -> CommandResult:
        # Idempotency check
        if self.command_repo.exists(command.idempotency_key):
            return CommandResult(status='already_processed')

        # Business rule validation against write model
        self.validator.validate(command)

        # Persist command record
        cmd_record = self.command_repo.insert(
            type=command.type,
            payload=command.payload,
            status='pending',
            idempotency_key=command.idempotency_key
        )

        # Publish domain event to Kafka
        event = DomainEvent(
            id=uuid4(),
            command_id=cmd_record.id,
            type=command.event_type(),
            payload=command.payload,
            occurred_at=utcnow()
        )
        self.event_bus.publish(topic='domain-events', event=event)

        # Update write model (normalized PostgreSQL)
        self.write_model.apply(command)

        self.command_repo.update_status(cmd_record.id, 'processed')
        return CommandResult(status='accepted', command_id=cmd_record.id)

Command Table Schema

CREATE TABLE commands (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    type            VARCHAR(100) NOT NULL,
    payload         JSONB NOT NULL,
    status          VARCHAR(20) NOT NULL DEFAULT 'pending',
                    -- pending | processed | failed
    idempotency_key VARCHAR(255) UNIQUE NOT NULL,
    created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
    processed_at    TIMESTAMPTZ
);

CREATE INDEX idx_commands_status ON commands(status);
CREATE INDEX idx_commands_idempotency ON commands(idempotency_key);

Query Side

Read Model

The read model is a denormalized projection stored in PostgreSQL or Redis, rebuilt from domain events by the ProjectionWorker. Queries bypass the write model entirely.

class ProjectionWorker:
    def __init__(self):
        self.consumer = KafkaConsumer(
            topic='domain-events',
            group_id='projection-worker',
            auto_offset_reset='earliest'
        )

    def run(self):
        for event in self.consumer:
            if self.processed_events.contains(event.id):
                continue  # Idempotent processing
            self.projector.project(event)
            self.processed_events.mark(event.id)
            self.consumer.commit()

class OrderProjector:
    def project(self, event: DomainEvent):
        if event.type == 'OrderCreated':
            self.read_db.upsert('order_views', {
                'order_id': event.payload['order_id'],
                'customer_name': self.lookup_customer(event.payload['customer_id']),
                'total': event.payload['total'],
                'status': 'created',
                'updated_at': event.occurred_at
            })
        elif event.type == 'OrderShipped':
            self.read_db.update('order_views',
                where={'order_id': event.payload['order_id']},
                set={'status': 'shipped', 'updated_at': event.occurred_at}
            )

Idempotent Event Processing

CREATE TABLE processed_events (
    event_id    UUID PRIMARY KEY,
    processed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

API Routing

# Command endpoint → command bus
POST /api/orders          → CommandHandler.handle(CreateOrderCommand)
POST /api/orders/:id/ship → CommandHandler.handle(ShipOrderCommand)

# Query endpoint → read model directly
GET /api/orders           → ReadModelRepository.list_orders()
GET /api/orders/:id       → ReadModelRepository.get_order(id)

Eventual Consistency

The read model is typically 50–200ms behind the write model, depending on Kafka consumer lag and projection throughput. This window must be acceptable to the business use case.

Projector Lag Monitoring

# Monitor Kafka consumer group lag
kafka-consumer-groups.sh 
  --bootstrap-server kafka:9092 
  --describe 
  --group projection-worker

# Alert if lag > threshold
class LagMonitor:
    ALERT_THRESHOLD = 1000  # messages

    def check(self):
        lag = self.kafka_admin.consumer_group_lag('projection-worker')
        if lag > self.ALERT_THRESHOLD:
            self.alerting.fire('ProjectionLagHigh', lag=lag)

Compensating Commands

When a command fails mid-way or a business rule is violated post-hoc, compensating commands reverse the effect.

class CancelOrderCommand(Command):
    """Compensating command for CreateOrder"""
    def event_type(self): return 'OrderCancelled'

class RefundPaymentCommand(Command):
    """Compensating command for ChargePayment"""
    def event_type(self): return 'PaymentRefunded'

Scaling Considerations

  • Command side: scale horizontally; stateless handlers, DB is the consistency boundary
  • Query side: scale read replicas or Redis cache; reads never touch the write DB
  • Kafka partitioning: partition by aggregate ID (e.g., order_id) to preserve ordering per aggregate
  • Multiple projections: same event stream can feed multiple independent read models (e.g., order view, analytics, search index)

Key Design Decisions

  • Write model uses normalized PostgreSQL for transactional integrity
  • Read model uses denormalized PostgreSQL or Redis for query performance
  • Kafka provides durable, replayable event log — projections can be rebuilt from scratch
  • Idempotency key on commands prevents duplicate processing under retries
  • Separate processed_events table makes projection idempotent under consumer restarts

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is CQRS and why is it used in system design?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “CQRS (Command Query Responsibility Segregation) separates the write model (commands) from the read model (queries). It's used when read and write workloads have different scaling, performance, or consistency requirements. Commands mutate state and emit events; queries read from a denormalized projection optimized for fast retrieval.”
}
},
{
“@type”: “Question”,
“name”: “How does the read model stay in sync with the write model in CQRS?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “A ProjectionWorker consumes domain events from Kafka and updates the read model (denormalized PostgreSQL or Redis). The read model is eventually consistent, typically 50'200ms behind the write model depending on consumer lag and projection throughput.”
}
},
{
“@type”: “Question”,
“name”: “How is idempotency handled in a CQRS service?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Commands include a unique idempotency_key stored in the commands table with a UNIQUE constraint. The ProjectionWorker tracks processed event IDs in a processed_events table, ensuring each event is applied exactly once even under at-least-once Kafka delivery.”
}
},
{
“@type”: “Question”,
“name”: “How do companies like Atlassian, Amazon, and Uber use CQRS in production?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Atlassian uses CQRS-style separation in Jira's issue tracking to scale reads independently from writes. Amazon applies it in order management where order placement (command) and order status queries (read model) are separated for performance. Uber uses event-driven projections to power rider and driver dashboards with low-latency reads from denormalized read models.”
}
}
]
}

See also: Atlassian Interview Guide

See also: Scale AI Interview Guide 2026: Data Infrastructure, RLHF Pipelines, and ML Engineering

See also: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering

Scroll to Top