Mastering Domain-Driven Design DDD Patterns in Spring Boot Applications

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.

Mastering Domain-Driven Design DDD Patterns in Spring Boot Applications

🧱 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

PatternDescription
EntityObject with a unique identifier (e.g., Order, Customer)
Value ObjectImmutable, no identity (e.g., Money, Address)
AggregateGroup of related objects treated as a unit
Domain ServiceStateless service containing business logic not belonging to entities
RepositoryInterface 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

BenefitWhy it matters
Aligned with businessReflects domain terms and behaviors
MaintainabilityEasier to modify logic with clear boundaries
TestabilityBusiness logic is testable without the web/db
Separation of concernsDecouples 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.

πŸ”— External References