Written for absolute beginners. Every concept is explained in plain English first, followed by a real-world analogy, ASCII diagram, and Java code example. We build one real system throughout — an Online Food Delivery App (like Swiggy or Zomato). At the end you will find 50 interview questions with detailed answers.
Imagine you are hired to build software for a hospital. The hospital has doctors, patients, appointments, prescriptions, billing, and insurance. All of this real-world business territory — the problem your software must solve — is called the Domain.
Domain-Driven Design (DDD) is a way of building software where the business problem drives every design decision. The code should mirror the real world so closely that a business person — someone who has never written code — can read it and understand what it does.
It was introduced by Eric Evans in his 2003 book "Domain-Driven Design: Tackling Complexity in the Heart of Software."
WITHOUT DDD: WITH DDD:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Business says: Business says:
"The patient needs "The patient needs
a prescription" a prescription"
Developer writes: Developer writes:
createMedDoc( patient
patId, .issuePrescription(
drugCode, medication,
doseAmt, prescribingDoctor
freqCode );
);
Result: Result:
Only Raj understands ANY business person can
what this code does. read this code.
Business logic is hidden. Business logic is obvious.
Bugs are invisible. Bugs are obvious.
┌─────────────────────────────────────────────────────────┐
│ BUILDING A HOUSE — WITHOUT DDD │
│ │
│ Architect draws blueprints using "architect language" │
│ Builder receives blueprints and guesses what they mean │
│ Communication breaks down → wrong rooms, wrong doors │
│ Cost overruns → delays → bugs │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ BUILDING A HOUSE — WITH DDD │
│ │
│ Architect and Builder meet DAILY │
│ They agree: "load-bearing wall" means one specific │
│ thing. "Wet room" means one specific thing. │
│ Everyone uses the SAME WORDS. │
│ Builder writes exactly what architect designed. │
└─────────────────────────────────────────────────────────┘
As software grows, business logic becomes scattered, duplicated, and buried under technical noise. Teams working on different parts of the system can no longer agree on what things mean. Changing one part breaks another.
DDD solves this by giving teams:
┌────────────────────────────────────────────────────────────────────┐
│ THREE PILLARS OF DDD │
│ │
│ ┌────────────────────┐ ┌─────────────────────┐ ┌──────────────┐ │
│ │ 1. FOCUS ON THE │ │ 2. COLLABORATE WITH │ │ 3. SPEAK ONE │ │
│ │ CORE DOMAIN │ │ DOMAIN EXPERTS │ │ LANGUAGE │ │
│ │ │ │ │ │ │ │
│ │ Your business │ │ Developers sit with │ │ Same words │ │
│ │ logic is your │ │ business people │ │ in meetings,│ │
│ │ most valuable │ │ and model together. │ │ code, docs, │ │
│ │ asset. Protect │ │ NOT in isolation. │ │ and tickets.│ │
│ │ it fiercely. │ │ │ │ │ │
│ └────────────────────┘ └─────────────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────────────┘
Domain Experts are people who deeply understand the business — NOT developers. They are the people whose knowledge you are encoding into software.
Our Food Delivery App — Domain Experts:
👤 Operations Manager → knows how orders flow from placement to delivery
👤 Restaurant Partner → knows how menus work, prep times, item availability
👤 Delivery Head → knows routing, partner assignment, distance calculation
👤 Finance Manager → knows payment methods, refund rules, tax calculation
👤 Customer Support → knows cancellation rules, complaint resolution
Ubiquitous means found everywhere. Ubiquitous Language is a shared vocabulary agreed upon by both developers and domain experts, then used in EVERY place:
SAME CONCEPT — DIFFERENT WORDS (Chaos):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Operations Manager says: "The customer PLACED an ORDER"
Product Manager tracks: "USER PURCHASE ticket #8823"
Backend Developer wrote: class FoodTransaction { }
Database table: tbl_food_requests
REST endpoint: POST /api/v1/buy-food
Kafka topic: food-order-events-v3
6 different words for the same thing = confusion, bugs,
wrong software built, expensive to maintain.
AGREED GLOSSARY — FOOD DELIVERY APP:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Term Meaning ❌ Do NOT say
──────────────── ────────────────────────────── ────────────────
Customer Person who places food orders User, Client
Restaurant Food vendor registered on app Merchant, Seller
Order Confirmed request for food items Purchase, Request
Order Item One dish + quantity in an order Line item, Entry
Cart Temporary pre-order collection Basket, Buffer
Place (an order) Submit cart → confirmed order Create, Checkout
Confirm Restaurant accepts the order Accept, Approve
Prepare Kitchen cooking the food Cook, Process
Dispatch Food handed to delivery partner Ship, Send
Deliver Food reaches the customer Complete, Drop
Cancel Order stopped before dispatch Abort, Reverse
Delivery Partner Person physically delivering Driver, Rider
Delivery Fee Cost charged for delivery Shipping cost
Rating Customer's score for the order Review, Feedback
// ❌ BAD — technical language, meaningless to business
public class FoodTransaction {
private String txnId;
private String userId;
private String statusCode;
private double totalAmt;
public void processTxn() { }
public void markCompleted() { }
public void flagForRefund() { }
}
// ✅ GOOD — business language in code
public class Order {
private OrderId orderId;
private CustomerId customerId;
private OrderStatus status;
private Money totalAmount;
public void place() { }
public void confirmByRestaurant() { }
public void cancel(CancellationReason reason) { }
public void deliver() { }
}
Apply EVERYWHERE:
Java class: class Order { }
Method name: void confirmByRestaurant()
REST API: POST /api/orders
DB table: orders
Kafka topic: order.placed
Jira ticket: "Order cancellation does not trigger refund"
Daily standup: "The order confirmation flow is failing"
The Domain is the entire subject area your software addresses — everything about your business.
┌────────────────────────────────────────────────────────────┐
│ FOOD DELIVERY APP — FULL DOMAIN │
│ │
│ Customers, Restaurants, Menus, Orders, Payments, │
│ Delivery, Tracking, Reviews, Promotions, Notifications, │
│ Analytics, Customer Support, Driver Management │
└────────────────────────────────────────────────────────────┘
No single team can own everything. We split the domain into Subdomains — logical groupings of related capabilities.
┌─────────────────────────────────────────────────────────────────────┐
│ FOOD DELIVERY APP — SUBDOMAIN MAP │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ CORE SUBDOMAINS ⭐ │ │
│ │ Your competitive advantage. BUILD IN-HOUSE. │ │
│ │ │ │
│ │ 📦 Order Management — the heart of the platform │ │
│ │ 🗺️ Smart Delivery Routing — AI-based shortest path │ │
│ │ 💰 Dynamic Pricing — surge pricing, discounts │ │
│ │ ⭐ Restaurant Ranking — why some show up first │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SUPPORTING SUBDOMAINS │ │
│ │ Necessary but not your differentiator. │ │
│ │ Build or customise off-the-shelf. │ │
│ │ │ │
│ │ 👤 Customer Profiles 🍽️ Restaurant Onboarding │ │
│ │ 📋 Menu Management 🚗 Delivery Partner Mgmt │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ GENERIC SUBDOMAINS │ │
│ │ Same in every business. BUY SaaS — don't build yourself. │ │
│ │ │ │
│ │ 🔐 Authentication → Auth0 / Cognito │ │
│ │ 📧 Email / SMS → SendGrid / Twilio │ │
│ │ 💳 Payments → Razorpay / Stripe │ │
│ │ 📊 Analytics → Mixpanel / Amplitude │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
A Bounded Context is a clear boundary inside which ONE domain model applies with ONE set of agreed meanings. The same word can mean something completely different in a different Bounded Context.
Real-world analogy: The word "interest" means:
- In a bank's savings department: money they pay YOU
- In a bank's loans department: money YOU pay them
- In HR: your hobbies and passions
Same word. Three different bounded contexts. Three completely different meanings.
┌───────────────────────┬───────────────────────┬───────────────────────┐
│ ORDER CONTEXT │ PAYMENT CONTEXT │ DELIVERY CONTEXT │
│ │ │ │
│ Order = { │ Order = { │ Order = { │
│ orderId, │ orderId, ← ID only│ orderId, ← ID only│
│ customerId, │ amount, │ pickupAddress, │
│ restaurantId, │ currency, │ dropAddress, │
│ orderItems[], │ paymentMethod, │ partnerId, │
│ deliveryAddress, │ taxAmount, │ estimatedMinutes, │
│ specialNote, │ invoiceNumber │ trackingUrl │
│ status, │ } │ } │
│ placedAt │ │ │
│ } │ Cares about MONEY, │ Cares about ROUTING, │
│ │ not food │ not food or money │
│ Cares about FOOD │ │ │
│ and delivery details │ │ │
└───────────────────────┴───────────────────────┴───────────────────────┘
INSIGHT: Each context has a focused, lean model of "Order"
without being polluted by other contexts' concerns.
This keeps each model simple, clear, and correct.
┌──────────────────────┐
│ MENU CONTEXT │
│ Menu, Category, │
│ MenuItem, Price │
└──────────┬───────────┘
│ provides menu data
▼
┌────────────────────────────────┐
│ ORDER CONTEXT ⭐ │
│ Place, Confirm, Cancel, │
│ Dispatch, Deliver │
└────┬────────────────┬───────────┘
│ │
OrderPlaced│event │OrderPlaced event
▼ ▼
┌──────────────┐ ┌───────────────────┐
│ PAYMENT │ │ RESTAURANT │
│ CONTEXT │ │ CONTEXT │
│ Charge, │ │ Accept, Prepare, │
│ Refund, │ │ Mark Ready │
│ Invoice │ │ │
└──────┬───────┘ └─────────┬──────────┘
│ │
│ PaymentSuccess │ FoodReady event
│ event │
└──────────┬─────────┘
▼
┌────────────────────┐
│ DELIVERY CONTEXT │
│ Assign Partner, │
│ Track, Deliver │
└─────────┬──────────┘
│ all events
▼
┌────────────────────┐
│ NOTIFICATION CTX │
│ Email, SMS, Push │
└────────────────────┘
DDD organises code into four distinct layers. Each layer has a single, clear responsibility and strict rules about what it can depend on.
┌──────────────────────────────────────────────────────────────────────┐
│ USER INTERFACE LAYER │
│ │
│ REST Controllers, GraphQL Resolvers, CLI Commands, Gradio UI │
│ │
│ Role: Accept requests from the outside world. Present results. │
│ Rule: Contains ZERO business logic. Just translates and delegates. │
└──────────────────────────────────┬───────────────────────────────────┘
│ calls
┌──────────────────────────────────▼───────────────────────────────────┐
│ APPLICATION LAYER │
│ │
│ Use Cases: PlaceOrderService, CancelOrderService, GetOrderService │
│ │
│ Role: Orchestrate a complete use case. Load objects from │
│ repository → call domain logic → save → publish events. │
│ Rule: NO business rules. THIN layer. Just coordination. │
└──────────────────────────────────┬───────────────────────────────────┘
│ calls
┌──────────────────────────────────▼───────────────────────────────────┐
│ DOMAIN LAYER ⭐ THE HEART │
│ │
│ Entities, Value Objects, Aggregates, Domain Events, │
│ Domain Services, Repository Interfaces (not implementations) │
│ │
│ Role: ALL business rules and business logic live here. │
│ Rule: Pure Java/Kotlin — NO Spring annotations, NO JPA, │
│ NO HTTP, NO Kafka. Zero framework dependencies. │
└──────────────────────────────────┬───────────────────────────────────┘
│ implements interfaces from Domain
┌──────────────────────────────────▼───────────────────────────────────┐
│ INFRASTRUCTURE LAYER │
│ │
│ JPA Repositories, REST clients, Kafka producers/consumers, │
│ Email services, S3, Redis, external API adapters │
│ │
│ Role: All technical plumbing. Databases, messaging, external APIs. │
│ Rule: Implements interfaces defined in Domain/Application layers. │
└──────────────────────────────────────────────────────────────────────┘
DEPENDENCY RULE:
Outer layers can depend on inner layers.
Inner layers NEVER depend on outer layers.
Domain layer depends on NOTHING except itself.
┌─────────────────────────────────────────────────────────────┐
│ User Interface Layer = WAITER │
│ Takes order from customer, brings food back. │
│ Does not cook. Does not decide what to cook. │
│ │
│ Application Layer = KITCHEN MANAGER │
│ Coordinates: "Table 4 needs Burger. Tell chef. Tell │
│ cashier. Update reservation system." Pure coordination. │
│ │
│ Domain Layer = CHEF │
│ Knows ALL recipes. Enforces ALL cooking rules. │
│ "Burger must reach 75°C internal. Never serve raw." │
│ Contains all business knowledge. │
│ │
│ Infrastructure Layer = SUPPLIERS & EQUIPMENT │
│ Ingredients from farms. Ovens, fridges, POS systems. │
│ Chef doesn't care which farm grows the tomatoes. │
│ Chef just needs good tomatoes (the interface). │
└─────────────────────────────────────────────────────────────┘
com.fooddelivery.orderservice/
│
├── domain/ ← ⭐ The Heart (no framework deps)
│ ├── model/
│ │ ├── order/
│ │ │ ├── Order.java Aggregate Root
│ │ │ ├── OrderItem.java Entity
│ │ │ ├── OrderId.java Value Object
│ │ │ ├── OrderStatus.java Enum
│ │ │ └── events/
│ │ │ ├── OrderPlacedEvent.java
│ │ │ └── OrderCancelledEvent.java
│ │ └── shared/
│ │ ├── Money.java Value Object
│ │ ├── Address.java Value Object
│ │ └── DomainEvent.java Interface
│ ├── service/
│ │ └── DeliveryFeePolicy.java Domain Service
│ └── repository/
│ └── OrderRepository.java Interface (contract only)
│
├── application/ ← Orchestration (thin)
│ ├── PlaceOrderService.java
│ ├── CancelOrderService.java
│ └── GetOrderService.java
│
└── infrastructure/ ← Technical plumbing
├── persistence/
│ └── JpaOrderRepository.java Implements OrderRepository
├── messaging/
│ └── KafkaEventPublisher.java
└── web/
└── OrderController.java
An Entity is a domain object that has a unique identity that remains constant for its entire lifetime, even as its other attributes change.
Real-world analogy: You are an Entity. You have an Aadhaar number (identity) that never changes. You can change your name, address, weight, job, phone number — you are still the same person because your Aadhaar number identifies you uniquely.
ENTITY — the IDENTITY makes it unique:
─────────────────────────────────────────────────────
Order #5001 — even if you add/remove items, change
the address, or update its status, it is ALWAYS
Order #5001. The ID is what matters.
Customer "Priya" with ID CUST-999 — even if she
changes her phone number or moves to a new city,
she is STILL CUST-999. Same account. Same history.
VALUE OBJECT — the VALUES make it unique:
─────────────────────────────────────────────────────
₹200 — You don't care WHICH ₹200 note you hold.
Any ₹200 is as good as any other ₹200.
No identity needed.
"123 MG Road, Pune" — Just a data point.
No tracking needed. Two addresses with same
values are completely interchangeable.
┌────────────────────────────────────────────────────────┐
│ ENTITY CHARACTERISTICS │
│ │
│ ✅ Has a UNIQUE ID (OrderId, CustomerId, etc.) │
│ ✅ Two entities with same ID = the SAME entity │
│ ✅ MUTABLE — attributes change over time │
│ ✅ Has a LIFECYCLE (PLACED → CONFIRMED → DELIVERED) │
│ ✅ Contains BUSINESS BEHAVIOUR — not just data │
│ ✅ PROTECTS invariants — throws exception on bad state │
│ ❌ NO public setters — state changes via named methods │
│ ❌ NEVER expose internal collections directly │
└────────────────────────────────────────────────────────┘
// domain/model/order/Order.java
public class Order {
// ══════════════════════════════════════════
// IDENTITY — assigned at creation, never changes
// ══════════════════════════════════════════
private final OrderId id;
// ══════════════════════════════════════════
// ATTRIBUTES — can change over lifecycle
// ══════════════════════════════════════════
private final CustomerId customerId; // Ref by ID only — NOT a Customer object
private final RestaurantId restaurantId; // Ref by ID only — NOT a Restaurant object
private List<OrderItem> orderItems;
private Address deliveryAddress; // Value Object
private OrderStatus status;
private Money totalAmount; // Value Object
private LocalDateTime placedAt;
private String specialInstructions;
// Domain Events collected during operations — published later
private final List<DomainEvent> domainEvents = new ArrayList<>();
// ══════════════════════════════════════════
// FACTORY METHOD — the only way to create a valid Order
// ══════════════════════════════════════════
public static Order place(
CustomerId customerId,
RestaurantId restaurantId,
List<OrderItemRequest> itemRequests,
Address deliveryAddress) {
// Business rule: Must have at least one item
if (itemRequests == null || itemRequests.isEmpty()) {
throw new EmptyOrderException("An order must contain at least one item");
}
OrderId id = OrderId.generate();
Order order = new Order(id, customerId, restaurantId, deliveryAddress);
for (OrderItemRequest req : itemRequests) {
order.orderItems.add(new OrderItem(
MenuItemId.of(req.menuItemId()),
req.name(),
Money.of(req.unitPrice()),
Quantity.of(req.quantity())
));
}
order.recalculateTotal();
order.status = OrderStatus.PLACED;
order.placedAt = LocalDateTime.now();
order.domainEvents.add(
new OrderPlacedEvent(id, customerId, restaurantId, order.totalAmount)
);
return order;
}
// ══════════════════════════════════════════
// BUSINESS OPERATIONS — each enforces its own rules
// ══════════════════════════════════════════
/** Restaurant has accepted this order and will prepare it. */
public void confirmByRestaurant() {
requireStatus(OrderStatus.PLACED, "confirm");
this.status = OrderStatus.CONFIRMED;
domainEvents.add(new OrderConfirmedEvent(this.id, this.restaurantId));
}
/** Kitchen has finished cooking — food is ready for pickup. */
public void markReadyForPickup() {
requireStatus(OrderStatus.CONFIRMED, "mark ready for pickup");
this.status = OrderStatus.READY_FOR_PICKUP;
domainEvents.add(new OrderReadyEvent(this.id));
}
/** Delivery partner has collected the food and is on the way. */
public void dispatch(DeliveryPartnerId partnerId) {
requireStatus(OrderStatus.READY_FOR_PICKUP, "dispatch");
Objects.requireNonNull(partnerId, "Delivery partner required for dispatch");
this.status = OrderStatus.ON_THE_WAY;
domainEvents.add(new OrderDispatchedEvent(this.id, partnerId));
}
/** Food has been physically handed to the customer. */
public void deliver() {
requireStatus(OrderStatus.ON_THE_WAY, "deliver");
this.status = OrderStatus.DELIVERED;
domainEvents.add(new OrderDeliveredEvent(this.id, this.customerId));
}
/**
* Cancel the order.
* Business rule: Cannot cancel once the order is ON_THE_WAY or DELIVERED.
*/
public void cancel(CancellationReason reason) {
if (status == OrderStatus.ON_THE_WAY || status == OrderStatus.DELIVERED) {
throw new OrderNotCancellableException(
"Cannot cancel order " + id + " — already " + status.displayName() +
". Please contact support for assistance."
);
}
this.status = OrderStatus.CANCELLED;
domainEvents.add(new OrderCancelledEvent(this.id, this.customerId, reason));
}
// ══════════════════════════════════════════
// PRIVATE HELPERS
// ══════════════════════════════════════════
private void requireStatus(OrderStatus required, String action) {
if (this.status != required) {
throw new InvalidOrderStateException(
"Cannot " + action + " — order is " + status + ", expected " + required
);
}
}
private void recalculateTotal() {
this.totalAmount = orderItems.stream()
.map(OrderItem::subtotal)
.reduce(Money.ZERO, Money::add);
}
// ══════════════════════════════════════════
// IDENTITY-BASED EQUALITY — Entities are equal when IDs match
// ══════════════════════════════════════════
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order other)) return false;
return id.equals(other.id);
}
@Override
public int hashCode() { return id.hashCode(); }
// ══════════════════════════════════════════
// GETTERS — no setters, state changes via methods only
// ══════════════════════════════════════════
public OrderId getId() { return id; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
public CustomerId getCustomerId(){ return customerId; }
public List<OrderItem> getOrderItems() {
return Collections.unmodifiableList(orderItems); // Never expose mutable list
}
public List<DomainEvent> getDomainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearDomainEvents() { domainEvents.clear(); }
private Order(OrderId id, CustomerId customerId,
RestaurantId restaurantId, Address deliveryAddress) {
this.id = Objects.requireNonNull(id);
this.customerId = Objects.requireNonNull(customerId);
this.restaurantId = Objects.requireNonNull(restaurantId);
this.deliveryAddress = Objects.requireNonNull(deliveryAddress);
this.orderItems = new ArrayList<>();
this.status = OrderStatus.DRAFT;
}
}
A Value Object has no identity. It is defined entirely by its values. Two Value Objects with identical values are completely interchangeable. They are always immutable — once created, they never change.
Real-world analogy: A ₹100 note. You don't care WHICH specific note you hold. Any ₹100 = any other ₹100. If you want ₹200, you don't edit the note — you get a NEW ₹200. That's how Value Objects work: replace, never mutate.
┌────────────────────────────────────────────────────────┐
│ VALUE OBJECT RULES │
│ │
│ ✅ No unique ID │
│ ✅ Compared by ALL VALUES (not reference or ID) │
│ ✅ IMMUTABLE — private final fields, no setters │
│ ✅ SELF-VALIDATING — validates in constructor │
│ ✅ REPLACED not updated (create new instance) │
│ ✅ Can have BEHAVIOUR (add, subtract, format) │
│ ✅ mark the class as FINAL │
│ │
│ Common examples: │
│ Money, Address, Email, PhoneNumber, Quantity, │
│ DateRange, GPS Coordinates, OrderId (typed ID) │
└────────────────────────────────────────────────────────┘
// ═══════════════════════════════════════════════════════════
// domain/model/shared/Money.java
// ═══════════════════════════════════════════════════════════
public final class Money {
public static final Money ZERO = new Money(BigDecimal.ZERO, "INR");
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
if (amount == null)
throw new IllegalArgumentException("Amount cannot be null");
if (currency == null || currency.isBlank())
throw new IllegalArgumentException("Currency cannot be blank");
if (amount.compareTo(BigDecimal.ZERO) < 0)
throw new IllegalArgumentException("Money amount cannot be negative: " + amount);
// Store with consistent scale
this.amount = amount.setScale(2, RoundingMode.HALF_UP);
this.currency = currency.toUpperCase();
}
public static Money of(double amount) { return new Money(BigDecimal.valueOf(amount), "INR"); }
public static Money ofINR(BigDecimal a) { return new Money(a, "INR"); }
// ── Behaviour — always returns NEW Money ─────────────
public Money add(Money other) {
assertSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money subtract(Money other) {
assertSameCurrency(other);
BigDecimal result = this.amount.subtract(other.amount);
if (result.compareTo(BigDecimal.ZERO) < 0)
throw new InsufficientAmountException("Cannot subtract " + other + " from " + this);
return new Money(result, this.currency);
}
public Money multiplyBy(int factor) {
return new Money(this.amount.multiply(BigDecimal.valueOf(factor)), this.currency);
}
public Money applyDiscountPercent(int percent) {
BigDecimal factor = BigDecimal.ONE
.subtract(BigDecimal.valueOf(percent).divide(BigDecimal.valueOf(100)));
return new Money(this.amount.multiply(factor), this.currency);
}
public boolean isGreaterThan(Money other) {
assertSameCurrency(other);
return this.amount.compareTo(other.amount) > 0;
}
public boolean isZero() { return amount.compareTo(BigDecimal.ZERO) == 0; }
// ── Equality by VALUE ─────────────────────────────────
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money m)) return false;
return amount.compareTo(m.amount) == 0 && currency.equals(m.currency);
}
@Override public int hashCode() { return Objects.hash(amount.stripTrailingZeros(), currency); }
@Override public String toString() { return "₹" + amount.toPlainString(); }
private void assertSameCurrency(Money other) {
if (!currency.equals(other.currency))
throw new CurrencyMismatchException(currency + " vs " + other.currency);
}
public BigDecimal getAmount() { return amount; }
public String getCurrency() { return currency; }
}
// ═══════════════════════════════════════════════════════════
// domain/model/shared/Address.java
// ═══════════════════════════════════════════════════════════
public final class Address {
private final String flatOrHouseNo;
private final String streetOrLocality;
private final String city;
private final String state;
private final String pinCode;
public Address(String flatOrHouseNo, String streetOrLocality,
String city, String state, String pinCode) {
this.flatOrHouseNo = requireNonBlank(flatOrHouseNo, "Flat/House No");
this.streetOrLocality = requireNonBlank(streetOrLocality, "Street/Locality");
this.city = requireNonBlank(city, "City");
this.state = requireNonBlank(state, "State");
this.pinCode = validatePin(pinCode);
}
private static String validatePin(String pin) {
if (pin == null || !pin.matches("\\d{6}"))
throw new InvalidAddressException("PIN code must be exactly 6 digits, got: " + pin);
return pin;
}
private static String requireNonBlank(String value, String fieldName) {
if (value == null || value.isBlank())
throw new InvalidAddressException(fieldName + " cannot be blank");
return value.trim();
}
public String singleLine() {
return flatOrHouseNo + ", " + streetOrLocality + ", "
+ city + ", " + state + " - " + pinCode;
}
// ── Equality by ALL VALUES ─────────────────────────────
@Override
public boolean equals(Object o) {
if (!(o instanceof Address a)) return false;
return Objects.equals(flatOrHouseNo, a.flatOrHouseNo)
&& Objects.equals(streetOrLocality, a.streetOrLocality)
&& Objects.equals(city, a.city)
&& Objects.equals(state, a.state)
&& Objects.equals(pinCode, a.pinCode);
}
@Override public int hashCode() {
return Objects.hash(flatOrHouseNo, streetOrLocality, city, state, pinCode);
}
public String getCity() { return city; }
public String getPinCode() { return pinCode; }
public String getState() { return state; }
}
// ═══════════════════════════════════════════════════════════
// domain/model/order/OrderId.java — Typed ID as Value Object
// ═══════════════════════════════════════════════════════════
public final class OrderId {
private final String value;
private OrderId(String value) {
if (value == null || value.isBlank())
throw new IllegalArgumentException("OrderId cannot be blank");
this.value = value;
}
public static OrderId generate() {
return new OrderId("ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
}
public static OrderId of(String value) { return new OrderId(value); }
@Override public boolean equals(Object o) {
if (!(o instanceof OrderId other)) return false;
return value.equals(other.value);
}
@Override public int hashCode() { return value.hashCode(); }
@Override public String toString() { return value; }
}
┌──────────────────────────────┬──────────────────────────────┐
│ ENTITY │ VALUE OBJECT │
├──────────────────────────────┼──────────────────────────────┤
│ Has a unique ID │ No identity at all │
│ Compared by ID │ Compared by all values │
│ Mutable over time │ Immutable — never changes │
│ Has a lifecycle │ Created and discarded │
│ │ │
│ Order, Customer, Restaurant, │ Money, Address, Quantity, │
│ DeliveryPartner │ OrderId, Email, PhoneNumber │
├──────────────────────────────┼──────────────────────────────┤
│ Order #5001 stays #5001 │ ₹100 is ₹100 — no matter │
│ even if its items change. │ which note. Replaceable. │
└──────────────────────────────┴──────────────────────────────┘
An Aggregate is a cluster of Entities and Value Objects that must be kept consistent together. It has one designated leader called the Aggregate Root — the single entry point for ALL changes inside the cluster.
Real-world analogy: An Order at a food delivery app is an Aggregate.
You cannot directly call the kitchen and change the burger quantity.
You must tell the WAITER (Aggregate Root) who updates the order.
The waiter ensures the total is recalculated, the status is valid, and all rules are followed.
The waiter is the gatekeeper.
WITHOUT AGGREGATES (Chaos):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Thread A: Adds item ₹200 → updates total to ₹700
Thread B: Removes item → updates total to ₹300
Thread C: Reads total = ??? (inconsistent data!)
Payment service charges ₹300
Delivery service calculates for ₹700 order
DATA CORRUPTION → Customer overcharged or undercharged
WITH AGGREGATES (Safety):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ALL changes go through Order (Aggregate Root).
Order.addItem() automatically recalculates total.
One transaction, one aggregate, one consistent state.
Total is ALWAYS equal to sum of items. Guaranteed.
┌──────────────────────────────────────────────────────────────────────┐
│ ORDER AGGREGATE │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ORDER (Aggregate Root ⭐) │ │
│ │ │ │
│ │ id: ORD-A1B2C3D4 ← Unique identity │ │
│ │ customerId: CUST-99 ← Reference by ID only! │ │
│ │ restaurantId: REST-12 ← Reference by ID only! │ │
│ │ status: PLACED │ │
│ │ totalAmount: ₹499.00 ← Auto-maintained │ │
│ │ placedAt: 2025-04-25 14:32 │ │
│ │ │ │
│ │ place() confirmByRestaurant() cancel() deliver() │ │
│ │ ↑ The ONLY gate into this aggregate from outside │ │
│ └──────────────┬─────────────────────────────┬────────────────┘ │
│ │ owns │ owns │
│ ┌──────────────▼──────────────────┐ ┌────────▼──────────────────┐ │
│ │ ORDER ITEMS (Entities) │ │ DELIVERY ADDRESS │ │
│ │ │ │ (Value Object) │ │
│ │ Item 1: Veg Burger × 2 │ │ │ │
│ │ ₹150 each = ₹300 │ │ B-12, FC Road, │ │
│ │ │ │ Pune - 411005 │ │
│ │ Item 2: Masala Fries × 1 │ │ │ │
│ │ ₹99 = ₹99 │ │ Immutable. If address │ │
│ │ │ │ changes, create a new │ │
│ │ Item 3: Pepsi × 2 │ │ Address object. │ │
│ │ ₹50 each = ₹100 │ └───────────────────────────┘ │
│ │ │ │
│ │ ← Cannot be modified directly │ │
│ │ Must go through Order root │ │
│ └─────────────────────────────────┘ │
│ │
│ AGGREGATE BOUNDARY: The dashed outer box. │
│ Nothing outside can directly touch OrderItems. │
│ All access must pass through Order (the root). │
└──────────────────────────────────────────────────────────────────────┘
╔════════════════════════════════════════════════════════════════╗
║ RULE 1: Reference Other Aggregates by ID ONLY ║
╠════════════════════════════════════════════════════════════════╣
║ ❌ class Order { Customer customer; } // Object reference ║
║ ✅ class Order { CustomerId customerId; }// ID reference ║
║ ║
║ Why? Object reference creates a dependency. If Customer ║
║ changes, Order breaks. With an ID, they are independent. ║
╠════════════════════════════════════════════════════════════════╣
║ RULE 2: One Transaction Modifies One Aggregate ║
╠════════════════════════════════════════════════════════════════╣
║ ❌ One @Transactional saves Order AND Inventory ║
║ ✅ Save Order → emit OrderPlacedEvent → Inventory reacts ║
║ ║
║ Why? Small transactions are fast, safe, and easy to reason ║
║ about. Use Domain Events for cross-aggregate updates. ║
╠════════════════════════════════════════════════════════════════╣
║ RULE 3: External Access Only Through the Aggregate Root ║
╠════════════════════════════════════════════════════════════════╣
║ ❌ orderItemRepo.updateQuantity(itemId, newQty) ║
║ ✅ order.updateItemQuantity(itemId, Quantity.of(newQty)) ║
║ ║
║ Why? The root enforces all business rules. Direct access ║
║ bypasses validation and creates inconsistent state. ║
╠════════════════════════════════════════════════════════════════╣
║ RULE 4: Keep Aggregates SMALL ║
╠════════════════════════════════════════════════════════════════╣
║ Include ONLY what MUST change together in one transaction. ║
║ ║
║ ❌ God Aggregate: Order + Customer + Restaurant + Driver ║
║ ✅ Small Aggregate: Order + OrderItems + DeliveryAddress ║
║ ║
║ Clue: If it doesn't need to be consistent with the root, ║
║ it probably belongs in a different aggregate. ║
╚════════════════════════════════════════════════════════════════╝
A Domain Event is a record of something significant that happened in your business domain — always stated in past tense. It is a fact. It happened. It cannot be undone.
Real-world analogy: At an airport, when a flight LANDS, an "Aircraft Landed" event occurs. Independently:
- The refuelling crew reacts by scheduling the fuel truck.
- The cleaning crew reacts by heading to the gate.
- The gate agent reacts by preparing for the next flight.
- Baggage handlers react by going to baggage claim.
Nobody told each team individually. The event told them all. Each reacted independently. This is exactly how Domain Events work in software.
COMMAND (a request — might be rejected): DOMAIN EVENT (a fact — already happened):
────────────────────────────────────────── ─────────────────────────────────────────────
PlaceOrder OrderPlaced
CancelOrder OrderCancelled
DispatchOrder OrderDispatched
• Future/present tense • Past tense
• Addressed to ONE specific handler • Published to MANY interested listeners
• Can FAIL — business can reject it • Cannot fail — it already happened
• "Please do this for me" • "This happened — react if you care"
Customer submits order
│
▼
Order.place() is called
│
▼
Order creates OrderPlacedEvent ←─────────────────────────────┐
│ │
▼ │
orderRepository.save(order) │
│ │
▼ │
DomainEventPublisher publishes to Kafka topic "order.placed" │
│ │
├──────────────────────────────────────────────────────┘
│
├──────────────────────────────────┐
│ │
▼ ▼
PAYMENT SERVICE hears it: RESTAURANT SERVICE hears it:
→ Authorise ₹499 payment → Push notification to restaurant
→ Fires PaymentAuthorisedEvent → "New order! Start preparing."
│ │
└──────────────┬───────────────────┘
│ (both events)
▼
NOTIFICATION SERVICE hears it:
→ Sends: "Order #ORD-A1B2 placed!
Payment ₹499 received.
Estimated delivery: 35 mins."
→ SMS + Email + Push notification
// ═════════════════════════════════════════════════════════
// domain/model/shared/DomainEvent.java — Marker Interface
// ═════════════════════════════════════════════════════════
public interface DomainEvent {
String getEventId();
LocalDateTime getOccurredAt();
String getEventType();
}
// ═════════════════════════════════════════════════════════
// domain/model/order/events/OrderPlacedEvent.java
// ═════════════════════════════════════════════════════════
public class OrderPlacedEvent implements DomainEvent {
private final String eventId;
private final OrderId orderId;
private final CustomerId customerId;
private final RestaurantId restaurantId;
private final Money totalAmount;
private final LocalDateTime occurredAt;
public OrderPlacedEvent(OrderId orderId, CustomerId customerId,
RestaurantId restaurantId, Money totalAmount) {
this.eventId = UUID.randomUUID().toString();
this.orderId = orderId;
this.customerId = customerId;
this.restaurantId = restaurantId;
this.totalAmount = totalAmount;
this.occurredAt = LocalDateTime.now();
}
@Override public String getEventId() { return eventId; }
@Override public LocalDateTime getOccurredAt() { return occurredAt; }
@Override public String getEventType() { return "ORDER_PLACED"; }
public OrderId getOrderId() { return orderId; }
public CustomerId getCustomerId() { return customerId; }
public RestaurantId getRestaurantId() { return restaurantId; }
public Money getTotalAmount() { return totalAmount; }
}
A Repository is an abstraction that gives your domain layer the illusion of working with an in-memory collection of domain objects. It completely hides how objects are stored.
Real-world analogy: A librarian. You say: "Give me all books about Python published after 2020." You don't care if books are on wooden shelves, digital archives, or in a warehouse in another city. The librarian handles storage details. You just get the books.
// domain/repository/OrderRepository.java
/**
* Contract for Order persistence.
* This interface lives in the DOMAIN layer.
* It speaks the domain language — no SQL, no JPA, no technical details.
*/
public interface OrderRepository {
void save(Order order);
void delete(OrderId orderId);
Optional<Order> findById(OrderId orderId);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findByRestaurantId(RestaurantId restaurantId);
List<Order> findByStatus(OrderStatus status);
List<Order> findActiveOrdersForDeliveryPartner(DeliveryPartnerId partnerId);
boolean existsById(OrderId orderId);
}
// infrastructure/persistence/JpaOrderRepository.java
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderJpaRepo jpaRepo;
private final OrderPersistenceMapper mapper;
@Override
public void save(Order order) {
OrderJpaEntity entity = mapper.toJpa(order);
jpaRepo.save(entity);
}
@Override
public Optional<Order> findById(OrderId orderId) {
return jpaRepo.findById(orderId.toString())
.map(mapper::toDomain); // Convert JPA entity → domain object
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return jpaRepo.findByCustomerId(customerId.toString())
.stream()
.map(mapper::toDomain)
.toList();
}
// ... remaining methods
}
// Spring Data JPA interface — infrastructure detail only
interface SpringDataOrderJpaRepo extends JpaRepository<OrderJpaEntity, String> {
List<OrderJpaEntity> findByCustomerId(String customerId);
List<OrderJpaEntity> findByStatus(String status);
}
A Domain Service contains business logic that does not naturally belong to a single Entity or Value Object — it operates across multiple domain objects and represents a real business concept.
Real-world analogy: A referee in cricket. When there's a dispute (was it out or not?), you don't ask the batsman or the bowler — that's biased. The REFEREE applies the rules across both players. The referee is the Domain Service.
USE a Domain Service WHEN:
✅ Logic involves MULTIPLE aggregates
✅ The operation has a meaningful BUSINESS NAME
✅ It would feel WRONG to put it on any one entity
DO NOT use Domain Service for:
❌ Logic that belongs to ONE entity → put it on the entity
❌ Orchestration (load, call, save) → Application Service
❌ Database or email → Infrastructure
// domain/service/DeliveryFeePolicy.java
/**
* Calculates delivery fee for an order.
* This is a Domain Service because it operates across
* Order, Customer, Restaurant, and distance — no single
* entity is the right home for this logic.
*/
public class DeliveryFeePolicy {
/**
* Business Rules (as agreed with Operations Manager):
* 1. Swiggy One members always get free delivery
* 2. Free delivery if subtotal > ₹299
* 3. Base fee ₹20 for 0–3 km
* 4. +₹6 per km beyond 3 km
* 5. Premium restaurants add ₹30 surcharge
* 6. Maximum delivery fee is ₹80 (cap)
*/
public Money calculate(Order order, Customer customer,
Restaurant restaurant, DistanceKm distance) {
// Rule 1: Subscription members always free
if (customer.hasActiveSubscription(SubscriptionType.SWIGGY_ONE)) {
return Money.ZERO;
}
// Rule 2: Free above threshold
if (order.getTotalAmount().isGreaterThan(Money.of(299))) {
return Money.ZERO;
}
// Rule 3 & 4: Distance-based fee
Money fee;
if (distance.isWithin(3)) {
fee = Money.of(20);
} else {
int extraKm = distance.getValue() - 3;
fee = Money.of(20).add(Money.of(6 * extraKm));
}
// Rule 5: Premium restaurant surcharge
if (restaurant.isPremium()) {
fee = fee.add(Money.of(30));
}
// Rule 6: Cap at ₹80
Money cap = Money.of(80);
return fee.isGreaterThan(cap) ? cap : fee;
}
}
An Application Service is the orchestrator of a use case. It is deliberately thin — it loads domain objects, calls domain logic, saves results, and publishes events. It contains zero business rules.
Real-world analogy: An event manager at a wedding. They don't cook, decorate, or perform music. They coordinate: "Tell caterers to start. Inform DJ the couple has arrived. Seat the guests. Organise photographer." The specialists do the actual work. The event manager orchestrates.
Application Service = Pure Coordination
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Receive a COMMAND (PlaceOrderCommand)
2. LOAD domain objects from repository
3. CALL domain logic (entity methods, domain services)
4. SAVE changes back to repository
5. PUBLISH domain events
6. RETURN a result to the caller
❌ No if/else business conditions
❌ No money calculations
❌ No direct database queries
❌ No sending emails directly
// application/PlaceOrderService.java
@Service
@Transactional
public class PlaceOrderService {
private final OrderRepository orderRepository;
private final CustomerRepository customerRepository;
private final RestaurantRepository restaurantRepository;
private final DeliveryFeePolicy deliveryFeePolicy;
private final DomainEventPublisher eventPublisher;
// ── Input Command ──────────────────────────────────────
public record PlaceOrderCommand(
String customerId,
String restaurantId,
String flatNo, String street, String city, String state, String pinCode,
List<ItemRequest> items
) {
public record ItemRequest(String menuItemId, String name, double price, int qty) {}
}
// ── Output Result ─────────────────────────────────────
public record PlaceOrderResult(
String orderId,
double subtotal,
double deliveryFee,
double grandTotal
) {}
// ── The Use Case ──────────────────────────────────────
public PlaceOrderResult execute(PlaceOrderCommand cmd) {
// 1. LOAD domain objects
Customer customer = customerRepository
.findById(CustomerId.of(cmd.customerId()))
.orElseThrow(() -> new CustomerNotFoundException(cmd.customerId()));
Restaurant restaurant = restaurantRepository
.findById(RestaurantId.of(cmd.restaurantId()))
.orElseThrow(() -> new RestaurantNotFoundException(cmd.restaurantId()));
// 2. DOMAIN VALIDATION (domain object decides)
if (!restaurant.isAcceptingOrders()) {
throw new RestaurantNotAvailableException(
restaurant.getName() + " is not accepting orders right now."
);
}
// 3. BUILD value objects
Address deliveryAddress = new Address(
cmd.flatNo(), cmd.street(), cmd.city(), cmd.state(), cmd.pinCode()
);
List<OrderItemRequest> itemRequests = cmd.items().stream()
.map(i -> new OrderItemRequest(i.menuItemId(), i.name(), i.price(), i.qty()))
.toList();
// 4. CREATE aggregate — domain logic runs inside here
Order order = Order.place(
customer.getId(),
restaurant.getId(),
itemRequests,
deliveryAddress
);
// 5. DOMAIN SERVICE for cross-entity logic
DistanceKm distance = restaurant.distanceTo(deliveryAddress);
Money deliveryFee = deliveryFeePolicy.calculate(order, customer, restaurant, distance);
// 6. SAVE
orderRepository.save(order);
// 7. PUBLISH events
eventPublisher.publish(order.getDomainEvents());
order.clearDomainEvents();
// 8. RETURN plain result DTO
Money grandTotal = order.getTotalAmount().add(deliveryFee);
return new PlaceOrderResult(
order.getId().toString(),
order.getTotalAmount().getAmount().doubleValue(),
deliveryFee.getAmount().doubleValue(),
grandTotal.getAmount().doubleValue()
);
}
}
A Factory encapsulates complex object creation logic. When creating an Aggregate requires more than simple new, move that logic to a Factory.
// domain/service/OrderFactory.java
public class OrderFactory {
/**
* Creates an Order directly from a Customer's saved Cart.
* Encapsulates the conversion logic so it's not duplicated.
*/
public Order createFromCart(Cart cart, Address deliveryAddress) {
if (cart.isEmpty()) {
throw new EmptyCartException("Cannot create order from empty cart");
}
List<OrderItemRequest> requests = cart.getItems().stream()
.map(item -> new OrderItemRequest(
item.getMenuItemId().toString(),
item.getName(),
item.getUnitPrice().getAmount().doubleValue(),
item.getQuantity().getValue()
)).toList();
return Order.place(
cart.getCustomerId(),
cart.getRestaurantId(),
requests,
deliveryAddress
);
}
}
Context Mapping documents how different Bounded Contexts relate to and communicate with each other. It is the "foreign relations" map of your system.
┌─────────────────────────────────────────────────────────────────────┐
│ CONTEXT MAPPING PATTERNS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CUSTOMER-SUPPLIER │
│ Order Context (Downstream) consumes Menu Context (Upstream). │
│ Upstream provides, downstream consumes. │
│ Menu Context ──────────────────▶ Order Context │
│ │
│ ANTI-CORRUPTION LAYER (ACL) │
│ Protect your clean domain model from a messy external system. │
│ Translate between models at the boundary. │
│ Razorpay API ──[ACL Translator]──▶ Your PaymentGateway interface │
│ │
│ OPEN HOST SERVICE │
│ Publish a well-defined, stable API for many consumers. │
│ Order Service ──[REST API / Kafka Events]──▶ Many Services │
│ │
│ CONFORMIST │
│ You have no power to change the upstream. │
│ You adopt their model as-is (e.g., a government tax API). │
│ │
│ SHARED KERNEL │
│ Two closely-related teams share a small, common model. │
│ Changes require coordination between both teams. │
└─────────────────────────────────────────────────────────────────────┘
// Translating Razorpay's messy API into your clean domain language
// infrastructure/adapter/out/payment/RazorpayAdapter.java
@Component
public class RazorpayAdapter implements PaymentGateway { // PaymentGateway is YOUR interface
private final RazorpayApiClient razorpayClient;
@Override
public PaymentResult charge(Order order, PaymentDetails payment) {
// Translate YOUR domain → Razorpay format (Anti-Corruption)
RazorpayChargeRequest razorReq = new RazorpayChargeRequest(
order.getTotalAmount().getAmount()
.multiply(BigDecimal.valueOf(100)).intValue(), // They want paise!
"inr",
order.getId().toString(),
payment.getToken()
);
// Call external API
RazorpayChargeResponse razorResp = razorpayClient.charge(razorReq);
// Translate Razorpay format → YOUR domain (Anti-Corruption)
if ("captured".equals(razorResp.getStatus())) {
return PaymentResult.success(PaymentReference.of(razorResp.getPaymentId()));
}
return PaymentResult.failed(razorResp.getError().getDescription());
}
}
┌─────────────────────────────────────────────────────────────────┐
│ STRATEGIC DDD │
│ │
│ The big picture. Answers the question: │
│ "How do we structure our entire system?" │
│ │
│ Tools: Domain, Subdomains, Bounded Contexts, Context Maps │
│ │
│ Questions: │
│ • What are the different areas of our business? │
│ • Which is our core competitive advantage? │
│ • How do different parts communicate? │
│ • What do we build vs buy? │
│ │
│ Who: Architects + Tech Leads + Business Leaders │
│ When: BEFORE writing any code │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ TACTICAL DDD │
│ │
│ The implementation detail. Answers the question: │
│ "How do we model this Bounded Context in code?" │
│ │
│ Tools: Entities, Value Objects, Aggregates, Repositories, │
│ Domain Events, Domain Services, Application Services, │
│ Factories │
│ │
│ Questions: │
│ • Where does this business rule live? │
│ • How do we keep this data consistent? │
│ • How do parts communicate without tight coupling? │
│ │
│ Who: Developers + Domain Experts (pair programming) │
│ When: Inside each Bounded Context │
└─────────────────────────────────────────────────────────────────┘