Building complex applications requires more than just writing code β it demands strategic modeling of business domains. Thatβs where Domain-Driven Design (DDD) comes in. It encourages aligning code structure with business concepts using entities, value objects, aggregates, domain services, and bounded contexts.
In this post, youβll learn how to implement Domain-Driven Design (DDD) Patterns in Spring Boot. Weβll walk through practical examples to structure your codebase using tactical and strategic DDD patterns for maximum maintainability, scalability, and clarity.

π§± What is Domain-Driven Design?
Domain-Driven Design (DDD) is an architectural and modeling approach introduced by Eric Evans. It focuses on understanding and structuring code around business rules and domain logic.
π Tactical DDD Patterns
| Pattern | Description |
|---|---|
| Entity | Object with a unique identifier (e.g., Order, Customer) |
| Value Object | Immutable, no identity (e.g., Money, Address) |
| Aggregate | Group of related objects treated as a unit |
| Domain Service | Stateless service containing business logic not belonging to entities |
| Repository | Interface to access aggregates/entities |
ποΈ Project Structure
com.kscodes.springboot.advanced
β
βββ domain
β βββ model
β β βββ Order.java
β β βββ Address.java
β βββ repository
β β βββ OrderRepository.java
β βββ service
β βββ OrderService.java
β
βββ application
β βββ usecase
β βββ PlaceOrderUseCase.java
β
βββ infrastructure
β βββ persistence
β βββ JpaOrderRepository.java
β βββ JpaOrderEntity.java
β
βββ controller
βββ OrderController.java
π§ 1. Entity: Order.java
package com.kscodes.springboot.advanced.domain.model;
import java.util.UUID;
public class Order {
private final UUID id;
private final Address shippingAddress;
private OrderStatus status;
public Order(UUID id, Address shippingAddress) {
this.id = id;
this.shippingAddress = shippingAddress;
this.status = OrderStatus.CREATED;
}
public void markAsShipped() {
this.status = OrderStatus.SHIPPED;
}
// Getters
}
π¦ 2. Value Object: Address.java
package com.kscodes.springboot.advanced.domain.model;
import java.util.Objects;
public class Address {
private final String city;
private final String state;
private final String zip;
public Address(String city, String state, String zip) {
this.city = city;
this.state = state;
this.zip = zip;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address address = (Address) o;
return city.equals(address.city) && state.equals(address.state) && zip.equals(address.zip);
}
@Override
public int hashCode() {
return Objects.hash(city, state, zip);
}
}
π§© 3. Repository Interface
package com.kscodes.springboot.advanced.domain.repository;
import com.kscodes.springboot.advanced.domain.model.Order;
import java.util.Optional;
import java.util.UUID;
public interface OrderRepository {
Order save(Order order);
Optional findById(UUID id);
}
π§° 4. Domain Service
package com.kscodes.springboot.advanced.domain.service;
import com.kscodes.springboot.advanced.domain.model.Order;
import com.kscodes.springboot.advanced.domain.repository.OrderRepository;
import java.util.UUID;
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void shipOrder(UUID orderId) {
Order order = repository.findById(orderId).orElseThrow();
order.markAsShipped();
repository.save(order);
}
}
π 5. Application Use Case Layer
package com.kscodes.springboot.advanced.application.usecase;
import com.kscodes.springboot.advanced.domain.model.Address;
import com.kscodes.springboot.advanced.domain.model.Order;
import com.kscodes.springboot.advanced.domain.repository.OrderRepository;
import java.util.UUID;
public class PlaceOrderUseCase {
private final OrderRepository repository;
public PlaceOrderUseCase(OrderRepository repository) {
this.repository = repository;
}
public UUID execute(Address address) {
Order order = new Order(UUID.randomUUID(), address);
repository.save(order);
return order.getId();
}
}
π’ 6. Infrastructure β JPA Adapter
JpaOrderEntity.java
package com.kscodes.springboot.advanced.infrastructure.persistence;
import jakarta.persistence.*;
import java.util.UUID;
@Entity
@Table(name = "orders")
public class JpaOrderEntity {
@Id
private UUID id;
private String city;
private String state;
private String zip;
private String status;
// Getters and Setters
}
JpaOrderRepository.java
package com.kscodes.springboot.advanced.infrastructure.persistence;
import com.kscodes.springboot.advanced.domain.model.Address;
import com.kscodes.springboot.advanced.domain.model.Order;
import com.kscodes.springboot.advanced.domain.model.OrderStatus;
import com.kscodes.springboot.advanced.domain.repository.OrderRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.UUID;
@Repository
public class JpaOrderRepository implements OrderRepository {
private final SpringDataOrderRepository jpaRepo;
public JpaOrderRepository(SpringDataOrderRepository jpaRepo) {
this.jpaRepo = jpaRepo;
}
@Override
public Order save(Order order) {
JpaOrderEntity entity = new JpaOrderEntity();
entity.setId(order.getId());
entity.setCity(order.getShippingAddress().getCity());
entity.setState(order.getShippingAddress().getState());
entity.setZip(order.getShippingAddress().getZip());
entity.setStatus(order.getStatus().name());
jpaRepo.save(entity);
return order;
}
@Override
public Optional findById(UUID id) {
return jpaRepo.findById(id).map(entity -> new Order(
entity.getId(),
new Address(entity.getCity(), entity.getState(), entity.getZip())
));
}
}
π 7. Controller
package com.kscodes.springboot.advanced.controller;
import com.kscodes.springboot.advanced.application.usecase.PlaceOrderUseCase;
import com.kscodes.springboot.advanced.domain.model.Address;
import org.springframework.web.bind.annotation.*;
import java.util.UUID;
@RestController
@RequestMapping("/orders")
public class OrderController {
private final PlaceOrderUseCase useCase;
public OrderController(PlaceOrderUseCase useCase) {
this.useCase = useCase;
}
@PostMapping
public UUID placeOrder(@RequestBody Address address) {
return useCase.execute(address);
}
}
π‘οΈ Benefits of DDD in Spring Boot
| Benefit | Why it matters |
|---|---|
| Aligned with business | Reflects domain terms and behaviors |
| Maintainability | Easier to modify logic with clear boundaries |
| Testability | Business logic is testable without the web/db |
| Separation of concerns | Decouples model, infrastructure, and delivery |
π Conclusion
Implementing Domain-Driven Design (DDD) Patterns in Spring Boot helps you model complex domains more accurately and develop software that aligns with business needs. By separating concerns, following tactical patterns like entities, value objects, and repositories, and isolating domain logic from infrastructure, you prepare your application for scale and longevity.
Start applying DDD in smaller bounded contexts β and evolve your system architecture with clarity and purpose.