PromptForge Academy
dddcqrsarchitecturenew-project

CQRS and Domain-Driven Design with AI: A Prompt Engineering Approach

How to use Claude for Domain-Driven Design and CQRS: event storming facilitation, rich domain entities, and full CQRS vertical slices — with prompt patterns and a worked example that avoids anemic models.

Domain-Driven Design and CQRS reward precision — bounded contexts, aggregates, and command/query separation are all about drawing careful lines. That makes them a good fit for AI assistance, but also a place where an underspecified prompt produces exactly the anti-pattern DDD exists to prevent: anemic domain models with all the logic pushed into services.

The failure mode: AI-generated anemic models

Ask an LLM for "a User entity" with no constraints and you will typically get a plain data bag — public getters and setters, no invariants, no behavior. That's because most training data skews toward CRUD-style code. To get a rich domain model, the prompt has to explicitly demand invariant enforcement, private state, and behavior-carrying methods.

A prompt pattern for rich domain entities

  • State the invariants explicitly ("an Order cannot transition to Shipped unless it has at least one line item and a valid address").
  • Require the constructor/factory to reject invalid states rather than allowing them to be set later.
  • Ban public setters for anything that represents a business rule, not just a field.
  • Ask for domain events to be raised on meaningful state transitions if you're heading toward event sourcing or CQRS.

Worked example: from a vague ask to a real aggregate

Vague prompt

"Create an Order class for an e-commerce system." This produces a class with a List<LineItem>, an Add method with no validation, and a public Status setter — a bug waiting to happen, since nothing stops Status being set to Shipped with zero line items.

Structured prompt for a rich Order aggregate
Context: C#/.NET 8, using the aggregate pattern. Order is the aggregate root; LineItem is a child entity, never accessed directly from outside the aggregate.

Task: Design the Order aggregate.

Constraints:
- Invariant: Order cannot transition to Shipped unless it has at least one LineItem and a non-null ShippingAddress.
- Invariant: LineItems cannot be added once Order.Status is Shipped or Cancelled.
- No public setters on Status or LineItems — state changes only through named methods (AddLineItem, MarkAsShipped, Cancel) that enforce invariants and throw a domain-specific exception (OrderInvariantViolationException) on violation.
- Raise an OrderShipped domain event (with OrderId and ShippedAt) when MarkAsShipped succeeds, to be dispatched by the repository after save.

Output format: The Order class, the LineItem class, and the OrderInvariantViolationException — no repository or persistence code.
public sealed class Order
{
    private readonly List<LineItem> _lineItems = new();
    public IReadOnlyList<LineItem> LineItems => _lineItems;
    public OrderStatus Status { get; private set; } = OrderStatus.Draft;
    public Address? ShippingAddress { get; private set; }

    public void AddLineItem(LineItem item)
    {
        if (Status is OrderStatus.Shipped or OrderStatus.Cancelled)
            throw new OrderInvariantViolationException("Cannot add items after shipping or cancellation.");
        _lineItems.Add(item);
    }

    public void MarkAsShipped()
    {
        if (_lineItems.Count == 0 || ShippingAddress is null)
            throw new OrderInvariantViolationException("Order needs a line item and address to ship.");
        Status = OrderStatus.Shipped;
        // raise OrderShipped domain event here
    }
}

Event storming before code

Before generating any entity or command handler, AI is genuinely useful for facilitating an event-storming pass: surfacing candidate domain events, commands, and bounded-context boundaries from a plain-English description of the business process. This front-loads the architecture decisions that are expensive to change later.

Event storming facilitation prompt
Context: We're modeling the order-fulfillment process for an e-commerce platform: a customer places an order, payment is authorized, warehouse picks and packs, carrier ships, customer receives.

Task: Act as an event-storming facilitator. From this description, produce: (1) the candidate domain events in past tense, in chronological order, (2) the commands that trigger each event, (3) a first guess at bounded-context boundaries (e.g. Ordering, Payments, Fulfillment, Shipping), flagging any event that looks like it crosses a context boundary and might need an integration event instead of a domain event.

Generate the full vertical slice, not just the model

A CQRS feature is only useful end-to-end: command, handler, domain change, event, read-model projection. Prompting for the full slice in one structured request keeps the pieces consistent with each other rather than generating them in isolated, potentially mismatched calls.

  1. Command DTO and validator (e.g. ShipOrderCommand + FluentValidation rules).
  2. Handler that loads the aggregate, calls the domain method, persists, and dispatches raised events.
  3. Event handler(s) that update the read model (e.g. OrderShipped → update OrderSummaryReadModel.Status).
  4. A test that exercises the full slice: send the command, assert the read model reflects the change.

The New Project — Advanced program covers this full arc: event storming, rich domain modelling, and full CQRS vertical slices, alongside microservices and distributed-systems patterns.