From Sticky Notes to Stable Systems - How to Turn User Stories into Clean Domains, Use Cases, and Entities

You’ve got a wall (or Jira board) full of user stories.

“As a customer, I want to place an order so that I can buy products easily.”
“As an admin, I want to refund an order so that I can fix payment issues.”

Cool. But how do you go from this to a coherent codebase with well-defined domains, use cases, and entities instead of a ball of mud?

This article is a practical guide for software engineers on how to transform user stories into software step by step, using the lenses of DDD and Clean Architecture.


1. User Stories Are Not Design (They’re Signals)

A user story is:

A compact description of a desired outcome from the user’s perspective.

It gives you:

But it doesn’t tell you:

Treat user stories as signals of what matters, not as a direct mapping to routes, controllers, or database tables.


2. Step 1 — Enrich the Story Before You Model

Before jumping to code, deepen the story.

Take this example:

As a customer, I want to place an order so that I can buy products easily.

Ask more questions:

  1. Outcomes

    • What does “place an order” mean from the system’s perspective?
    • What state transitions happen? (e.g., “Cart” → “Order”)
  2. Rules

    • Can you place an order with out-of-stock items?
    • Do prices get “frozen” at the time of order?
    • What happens if payment fails?
  3. Happy path and edge cases

    • Card declined
    • Item goes out of stock between adding to cart and checkout
    • Address validation fails
  4. Events

    • “OrderPlaced”
    • “PaymentAuthorized”
    • “OrderRejected”

This gives you raw material to discover the domain, not just an endpoint.


3. Step 2 — Group Stories into Domains (Bounded Contexts)

You rarely have just one story. You have dozens, sometimes hundreds.
The next step is to cluster them into domains.

How to spot domains

Look for groups of stories that share:

Example e-commerce domains:

Goal: put user stories into domain buckets so you don’t design entities that span everything.


4. Step 3 — Extract Domain Concepts from the Stories

Now, within a single domain, start extracting concepts.

Take stories in the Ordering domain:

From these, you can identify candidate domain objects:

Write them down in simple language first:

At this stage you are not coding – you’re refining the domain language.


5. Step 4 — Derive Use Cases from User Stories

User stories talk about goals. Use cases talk about system behaviors.

From:

As a customer, I want to place an order…

You can derive use cases like:

Each use case:

Example (simplified TypeScript):

interface PlaceOrderCommand {
  customerId: string;
  items: { productId: string; quantity: number }[];
  shippingAddress: AddressDto;
}

interface PlaceOrderResult {
  orderId: string;
  status: "PENDING" | "PAID" | "FAILED";
}

class PlaceOrderUseCase {
  constructor(
    private readonly orderRepository: OrderRepository,
    private readonly pricingService: PricingService,
    private readonly paymentGateway: PaymentGateway
  ) {}

  async execute(command: PlaceOrderCommand): Promise<PlaceOrderResult> {
    const priceQuote = await this.pricingService.quote(command.items);

    const order = Order.create(
      command.customerId,
      command.items,
      command.shippingAddress,
      priceQuote.total
    );

    const paymentResult = await this.paymentGateway.charge(
      order.getPaymentRequest()
    );

    order.applyPaymentResult(paymentResult);

    await this.orderRepository.save(order);

    return {
      orderId: order.id.toString(),
      status: order.status,
    };
  }
}

Notice what this use case does not do:

It lives in the application layer, directly derived from the user stories.


6. Step 5 — Design Entities Around Invariants, Not Screens

Entities are not “tables with methods”.
Entities exist to protect invariants and represent business concepts.

From the Ordering domain, invariants might include:

Map these invariants to entity behavior:

class Order {
  private constructor(
    private readonly id: OrderId,
    private readonly customerId: CustomerId,
    private items: OrderItem[],
    private total: Money,
    private status: OrderStatus
  ) {}

  static create(
    customerId: string,
    itemRequests: { productId: string; quantity: number }[],
    shippingAddress: Address,
    total: Money
  ): Order {
    if (itemRequests.length === 0) {
      throw new Error("Order must contain at least one item");
    }

    const items = itemRequests.map(
      (r) => new OrderItem(ProductId.of(r.productId), Quantity.of(r.quantity))
    );

    return new Order(
      OrderId.generate(),
      CustomerId.of(customerId),
      items,
      total,
      OrderStatus.PENDING
    );
  }

  applyPaymentResult(result: PaymentResult) {
    if (result.success) {
      this.status = OrderStatus.PAID;
    } else {
      this.status = OrderStatus.FAILED;
    }
  }

  cancel() {
    if (!this.status.canBeCancelled()) {
      throw new Error("Order cannot be cancelled in current status");
    }

    this.status = OrderStatus.CANCELLED;
  }
}

Key point:
When you’re designing entities, ask:

“What must always be true in this business concept?”

Then encode that into methods and invariants.


7. Step 6 — Choose Aggregates and Boundaries Intentionally

Next, decide where one entity ends and another begins.

In the Ordering domain:

Why?

Heuristic:

If two things must be consistent at all times, they probably belong to the same aggregate.
If they’re just related but can drift or sync asynchronously, separate them into different aggregates or even domains.


8. Step 7 — Wire Use Cases to Infrastructure via Ports and Adapters

So far:

Now you connect use cases to the real world via ports and adapters.

Example:

interface OrderRepository {
  findById(id: OrderId): Promise<Order | null>;
  save(order: Order): Promise<void>;
}

class PostgresOrderRepository implements OrderRepository {
  // implements findById/save using SQL
}

Your PlaceOrderUseCase depends on OrderRepository (port), not PostgresOrderRepository (adapter).
This keeps your domain and use cases independent from tech choices.


9. A Concrete End-to-End Example

Let’s walk one story all the way down:

As a customer, I want to place an order so that I can buy products easily.

1) Discover domain & rules

2) Define use case

3) Design entities & aggregates

4) Define ports

5) Implement adapters

6) Connect UI

At the end, each line of code has a clear reason that traces back to the original user story.


10. Common Anti-Patterns (And How to Avoid Them)

1) Story → Endpoint → Table (direct mapping)

Fix:
Always ask: “What domain concept is this story touching?”
Model the domain first, then derive endpoints.


2) Use Cases That Are Just Thin Wrappers Around Repositories

// Bad: no domain logic, just persistence
class PlaceOrderUseCase {
  async execute(cmd) {
    await this.orderRepository.insert(cmd);
  }
}

Fix:
Move rules and invariants into entities and aggregates.
Let use cases coordinate but not replace domain logic.


3) Entities That Are Just Data Bags

class Order {
  id: string;
  status: string;
  // no methods, no invariants
}

Fix:
Ask: “What behavior belongs naturally to Order?”
Give it methods that enforce correctness.


4) Domains That Mirror Org Charts or Microservices by Default

Fix:
Find domains by language, rules, and change patterns, not just org charts.


11. Practical Mental Model

When you see a new user story, think in layers:

  1. User Story

    • What outcome and value does this describe?
  2. Domain

    • Which part of the business does this belong to?
    • What core concepts and rules are involved?
  3. Use Cases

    • What system actions are required?
    • What are their commands and results?
  4. Entities / Aggregates / Value Objects

    • What domain objects do we need?
    • What invariants must always hold?
  5. Ports & Adapters

    • What external systems do we depend on?
    • Which interfaces (ports) should we define?
    • How will we implement them (adapters)?
  6. UI / Facades

    • How will UI trigger use cases?
    • How will we present results?

If you follow this path, user stories stop being “tickets to implement” and become entry points to a well-structured domain model.