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:
- Who (actor)
- What (goal)
- Why (value)
But it doesn’t tell you:
- Where the domain boundaries are
- What the entities are
- What the invariants and rules are
- How the system should be decomposed
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:
-
Outcomes
- What does “place an order” mean from the system’s perspective?
- What state transitions happen? (e.g., “Cart” → “Order”)
-
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?
-
Happy path and edge cases
- Card declined
- Item goes out of stock between adding to cart and checkout
- Address validation fails
-
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:
- Vocabulary
- “cart, product, price, promotion” → Sales or Shopping domain
- “invoice, refund, ledger, payout” → Billing/Finance domain
- Data ownership
- Who owns the lifecycle of “Order”? Sales or Billing?
- Change patterns
- Features that change together over time usually belong together.
- Business processes
- “Browse → Add to Cart → Checkout → Pay” is often distinct from “Invoice → Reconcile → Refund”.
Example e-commerce domains:
- Catalog (products, categories, attributes)
- Shopping (cart, checkout)
- Ordering (orders, order lines, statuses)
- Payments (payments, refunds)
- Customer Accounts (profiles, addresses)
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:
- As a customer, I want to place an order…
- As a customer, I want to see my past orders…
- As an admin, I want to cancel an order…
From these, you can identify candidate domain objects:
- Entities
OrderOrderItem
- Value Objects
OrderId,CustomerId,Money,Address,OrderStatus
- Processes / Policies
- “When an order is canceled, stock is returned.”
- “An order cannot be canceled after it is shipped.”
Write them down in simple language first:
- An Order belongs to one Customer.
- An Order has one or more OrderItems.
- Each OrderItem refers to exactly one Product.
- An Order has a status: Pending, Paid, Shipped, Cancelled, etc.
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:
PlaceOrderUseCaseGetOrderDetailsUseCaseListCustomerOrdersUseCaseCancelOrderUseCase
Each use case:
- Has a specific input (DTO/command).
- Performs a business operation.
- Returns a result (DTO/response).
- Talks to repositories, not directly to the database.
- Manipulates entities and aggregates.
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 doesn’t render HTML or JSON.
- It doesn’t know which web framework you use.
- It doesn’t know which database you use.
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:
- An Order must have at least one item.
- An Order total must equal the sum of its items plus shipping minus discount.
- An Order in status “Shipped” cannot be canceled.
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:
Orderas aggregate rootOrderItemas child entityOrderStatus,OrderId,CustomerId,Moneyas value objects
Why?
- You want to change
Orderand itsOrderItemsin one transaction. - You want a single place (
Order) to enforce invariants: totals, cancellability, etc. - You don’t need independent lifecycle for
OrderItemoutside an Order.
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:
- User stories → domains
- Domains → entities & aggregates
- Stories → use cases
Now you connect use cases to the real world via ports and adapters.
- Ports: interfaces in the application/domain layers (
OrderRepository,PaymentGateway,PricingService). - Adapters: infrastructure implementations (Postgres, Stripe, third-party APIs).
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
- Belongs to Ordering domain.
- Needs
Order,OrderItem,CustomerId. - Rules about stock, prices, payment, cancellation.
2) Define use case
PlaceOrderUseCase- Input:
PlaceOrderCommand - Output:
PlaceOrderResult
3) Design entities & aggregates
Orderaggregate with OrderItems.- Keep invariants in
Order. OrderusesMoney,OrderStatus,CustomerId.
4) Define ports
OrderRepositoryPricingServicePaymentGateway
5) Implement adapters
PostgresOrderRepositoryStripePaymentGatewayPricingServiceHttpClient
6) Connect UI
- HTTP controller → calls
PlaceOrderUseCase. - Or frontend facade → sends command to backend API.
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)
- Problem: every story becomes a new endpoint + table, without any domain model.
- Effect: anemic entities, duplicated rules, hard-to-change systems.
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);
}
}
- Problem: business rules live in controllers, components, or random helpers.
- Effect: hard to test, hard to reason, no single source of truth.
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
}
- Problem: all logic goes to services; entity is just a DTO.
- Effect: everything depends on everything, duplication of “rules” across the codebase.
Fix:
Ask: “What behavior belongs naturally to Order?”
Give it methods that enforce correctness.
4) Domains That Mirror Org Charts or Microservices by Default
- “We have a payments team; let’s make everything payment-related its own microservice domain.”
- Fine for large organizations, but dangerous if you skip domain analysis.
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:
-
User Story
- What outcome and value does this describe?
-
Domain
- Which part of the business does this belong to?
- What core concepts and rules are involved?
-
Use Cases
- What system actions are required?
- What are their commands and results?
-
Entities / Aggregates / Value Objects
- What domain objects do we need?
- What invariants must always hold?
-
Ports & Adapters
- What external systems do we depend on?
- Which interfaces (ports) should we define?
- How will we implement them (adapters)?
-
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.