DEV Community

Dev Cookies
Dev Cookies

Posted on

DDD SpringBoot Full Project

Spring Boot DDD E-Commerce Order Management System

Project Structure

src/
├── main/
   ├── java/
      └── com/
          └── ecommerce/
              ├── ECommerceApplication.java
              ├── shared/
                 ├── domain/
                    ├── AggregateRoot.java
                    ├── DomainEvent.java
                    ├── Entity.java
                    └── ValueObject.java
                 ├── infrastructure/
                    └── EventPublisher.java
                 └── application/
                     └── UseCase.java
              ├── customer/
                 ├── domain/
                    ├── Customer.java
                    ├── CustomerId.java
                    ├── Email.java
                    └── CustomerRepository.java
                 ├── application/
                    ├── CustomerService.java
                    └── CreateCustomerUseCase.java
                 └── infrastructure/
                     ├── CustomerJpaRepository.java
                     ├── CustomerEntity.java
                     └── CustomerRepositoryImpl.java
              ├── product/
                 ├── domain/
                    ├── Product.java
                    ├── ProductId.java
                    ├── Money.java
                    └── ProductRepository.java
                 ├── application/
                    └── ProductService.java
                 └── infrastructure/
                     ├── ProductJpaRepository.java
                     ├── ProductEntity.java
                     └── ProductRepositoryImpl.java
              ├── order/
                 ├── domain/
                    ├── Order.java
                    ├── OrderId.java
                    ├── OrderItem.java
                    ├── OrderStatus.java
                    ├── OrderCreatedEvent.java
                    └── OrderRepository.java
                 ├── application/
                    ├── OrderService.java
                    ├── CreateOrderUseCase.java
                    └── dto/
                        ├── CreateOrderRequest.java
                        └── OrderResponse.java
                 └── infrastructure/
                     ├── OrderJpaRepository.java
                     ├── OrderEntity.java
                     ├── OrderItemEntity.java
                     └── OrderRepositoryImpl.java
              └── web/
                  ├── CustomerController.java
                  ├── ProductController.java
                  └── OrderController.java
   └── resources/
       ├── application.yml
       └── data.sql
└── test/
    └── java/
        └── com/
            └── ecommerce/
                └── order/
                    └── domain/
                        └── OrderTest.java
Enter fullscreen mode Exit fullscreen mode

1. Main Application Class

// ECommerceApplication.java
package com.ecommerce;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.ApplicationEventPublisher;
import com.ecommerce.shared.infrastructure.EventPublisher;

@SpringBootApplication
public class ECommerceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ECommerceApplication.class, args);
    }

    @Bean
    public EventPublisher eventPublisher(ApplicationEventPublisher publisher) {
        return new EventPublisher(publisher);
    }
}
Enter fullscreen mode Exit fullscreen mode

2. Shared Domain Layer

// shared/domain/Entity.java
package com.ecommerce.shared.domain;

import java.util.Objects;

public abstract class Entity<T> {
    protected T id;

    protected Entity(T id) {
        this.id = id;
    }

    public T getId() {
        return id;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Entity<?> entity = (Entity<?>) o;
        return Objects.equals(id, entity.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}
Enter fullscreen mode Exit fullscreen mode
// shared/domain/ValueObject.java
package com.ecommerce.shared.domain;

public abstract class ValueObject {
    // Value objects are immutable and compared by value
    @Override
    public abstract boolean equals(Object obj);

    @Override
    public abstract int hashCode();
}
Enter fullscreen mode Exit fullscreen mode
// shared/domain/AggregateRoot.java
package com.ecommerce.shared.domain;

import java.util.ArrayList;
import java.util.List;

public abstract class AggregateRoot<T> extends Entity<T> {
    private final List<DomainEvent> domainEvents = new ArrayList<>();

    protected AggregateRoot(T id) {
        super(id);
    }

    protected void addDomainEvent(DomainEvent event) {
        domainEvents.add(event);
    }

    public List<DomainEvent> getDomainEvents() {
        return new ArrayList<>(domainEvents);
    }

    public void clearDomainEvents() {
        domainEvents.clear();
    }
}
Enter fullscreen mode Exit fullscreen mode
// shared/domain/DomainEvent.java
package com.ecommerce.shared.domain;

import java.time.LocalDateTime;

public abstract class DomainEvent {
    private final LocalDateTime occurredOn;

    protected DomainEvent() {
        this.occurredOn = LocalDateTime.now();
    }

    public LocalDateTime getOccurredOn() {
        return occurredOn;
    }
}
Enter fullscreen mode Exit fullscreen mode
// shared/infrastructure/EventPublisher.java
package com.ecommerce.shared.infrastructure;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
import com.ecommerce.shared.domain.DomainEvent;

@Component
public class EventPublisher {
    private final ApplicationEventPublisher publisher;

    public EventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void publish(DomainEvent event) {
        publisher.publishEvent(event);
    }
}
Enter fullscreen mode Exit fullscreen mode

3. Customer Domain

// customer/domain/CustomerId.java
package com.ecommerce.customer.domain;

import com.ecommerce.shared.domain.ValueObject;
import java.util.Objects;
import java.util.UUID;

public class CustomerId extends ValueObject {
    private final String value;

    private CustomerId(String value) {
        this.value = value;
    }

    public static CustomerId generate() {
        return new CustomerId(UUID.randomUUID().toString());
    }

    public static CustomerId of(String value) {
        return new CustomerId(value);
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CustomerId that = (CustomerId) o;
        return Objects.equals(value, that.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
// customer/domain/Email.java
package com.ecommerce.customer.domain;

import com.ecommerce.shared.domain.ValueObject;
import java.util.Objects;
import java.util.regex.Pattern;

public class Email extends ValueObject {
    private static final Pattern EMAIL_PATTERN = 
        Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");

    private final String value;

    private Email(String value) {
        if (!EMAIL_PATTERN.matcher(value).matches()) {
            throw new IllegalArgumentException("Invalid email format");
        }
        this.value = value;
    }

    public static Email of(String value) {
        return new Email(value);
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Email email = (Email) o;
        return Objects.equals(value, email.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
// customer/domain/Customer.java
package com.ecommerce.customer.domain;

import com.ecommerce.shared.domain.AggregateRoot;

public class Customer extends AggregateRoot<CustomerId> {
    private String name;
    private Email email;

    private Customer(CustomerId id, String name, Email email) {
        super(id);
        this.name = name;
        this.email = email;
    }

    public static Customer create(String name, Email email) {
        return new Customer(CustomerId.generate(), name, email);
    }

    public static Customer restore(CustomerId id, String name, Email email) {
        return new Customer(id, name, email);
    }

    public String getName() {
        return name;
    }

    public Email getEmail() {
        return email;
    }

    public void updateName(String name) {
        this.name = name;
    }

    public void updateEmail(Email email) {
        this.email = email;
    }
}
Enter fullscreen mode Exit fullscreen mode
// customer/domain/CustomerRepository.java
package com.ecommerce.customer.domain;

import java.util.Optional;

public interface CustomerRepository {
    Customer save(Customer customer);
    Optional<Customer> findById(CustomerId id);
    Optional<Customer> findByEmail(Email email);
}
Enter fullscreen mode Exit fullscreen mode

4. Product Domain

// product/domain/ProductId.java
package com.ecommerce.product.domain;

import com.ecommerce.shared.domain.ValueObject;
import java.util.Objects;
import java.util.UUID;

public class ProductId extends ValueObject {
    private final String value;

    private ProductId(String value) {
        this.value = value;
    }

    public static ProductId generate() {
        return new ProductId(UUID.randomUUID().toString());
    }

    public static ProductId of(String value) {
        return new ProductId(value);
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductId productId = (ProductId) o;
        return Objects.equals(value, productId.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
// product/domain/Money.java
package com.ecommerce.product.domain;

import com.ecommerce.shared.domain.ValueObject;
import java.math.BigDecimal;
import java.util.Objects;

public class Money extends ValueObject {
    private final BigDecimal amount;
    private final String currency;

    private Money(BigDecimal amount, String currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }
        this.amount = amount;
        this.currency = currency;
    }

    public static Money of(BigDecimal amount, String currency) {
        return new Money(amount, currency);
    }

    public static Money usd(BigDecimal amount) {
        return new Money(amount, "USD");
    }

    public BigDecimal getAmount() {
        return amount;
    }

    public String getCurrency() {
        return currency;
    }

    public Money multiply(int quantity) {
        return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
    }

    public Money add(Money other) {
        if (!currency.equals(other.currency)) {
            throw new IllegalArgumentException("Cannot add different currencies");
        }
        return new Money(amount.add(other.amount), currency);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return Objects.equals(amount, money.amount) && 
               Objects.equals(currency, money.currency);
    }

    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
}
Enter fullscreen mode Exit fullscreen mode
// product/domain/Product.java
package com.ecommerce.product.domain;

import com.ecommerce.shared.domain.AggregateRoot;

public class Product extends AggregateRoot<ProductId> {
    private String name;
    private String description;
    private Money price;
    private int stockQuantity;

    private Product(ProductId id, String name, String description, Money price, int stockQuantity) {
        super(id);
        this.name = name;
        this.description = description;
        this.price = price;
        this.stockQuantity = stockQuantity;
    }

    public static Product create(String name, String description, Money price, int stockQuantity) {
        return new Product(ProductId.generate(), name, description, price, stockQuantity);
    }

    public static Product restore(ProductId id, String name, String description, Money price, int stockQuantity) {
        return new Product(id, name, description, price, stockQuantity);
    }

    public boolean isAvailable(int quantity) {
        return stockQuantity >= quantity;
    }

    public void reduceStock(int quantity) {
        if (!isAvailable(quantity)) {
            throw new IllegalStateException("Insufficient stock");
        }
        this.stockQuantity -= quantity;
    }

    // Getters
    public String getName() { return name; }
    public String getDescription() { return description; }
    public Money getPrice() { return price; }
    public int getStockQuantity() { return stockQuantity; }
}
Enter fullscreen mode Exit fullscreen mode
// product/domain/ProductRepository.java
package com.ecommerce.product.domain;

import java.util.Optional;
import java.util.List;

public interface ProductRepository {
    Product save(Product product);
    Optional<Product> findById(ProductId id);
    List<Product> findAll();
}
Enter fullscreen mode Exit fullscreen mode

5. Order Domain

// order/domain/OrderId.java
package com.ecommerce.order.domain;

import com.ecommerce.shared.domain.ValueObject;
import java.util.Objects;
import java.util.UUID;

public class OrderId extends ValueObject {
    private final String value;

    private OrderId(String value) {
        this.value = value;
    }

    public static OrderId generate() {
        return new OrderId(UUID.randomUUID().toString());
    }

    public static OrderId of(String value) {
        return new OrderId(value);
    }

    public String getValue() {
        return value;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderId orderId = (OrderId) o;
        return Objects.equals(value, orderId.value);
    }

    @Override
    public int hashCode() {
        return Objects.hash(value);
    }
}
Enter fullscreen mode Exit fullscreen mode
// order/domain/OrderStatus.java
package com.ecommerce.order.domain;

public enum OrderStatus {
    PENDING,
    CONFIRMED,
    SHIPPED,
    DELIVERED,
    CANCELLED
}
Enter fullscreen mode Exit fullscreen mode
// order/domain/OrderItem.java
package com.ecommerce.order.domain;

import com.ecommerce.product.domain.ProductId;
import com.ecommerce.product.domain.Money;
import com.ecommerce.shared.domain.ValueObject;
import java.util.Objects;

public class OrderItem extends ValueObject {
    private final ProductId productId;
    private final String productName;
    private final Money unitPrice;
    private final int quantity;

    public OrderItem(ProductId productId, String productName, Money unitPrice, int quantity) {
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }

    public Money getTotalPrice() {
        return unitPrice.multiply(quantity);
    }

    // Getters
    public ProductId getProductId() { return productId; }
    public String getProductName() { return productName; }
    public Money getUnitPrice() { return unitPrice; }
    public int getQuantity() { return quantity; }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OrderItem orderItem = (OrderItem) o;
        return quantity == orderItem.quantity &&
               Objects.equals(productId, orderItem.productId) &&
               Objects.equals(productName, orderItem.productName) &&
               Objects.equals(unitPrice, orderItem.unitPrice);
    }

    @Override
    public int hashCode() {
        return Objects.hash(productId, productName, unitPrice, quantity);
    }
}
Enter fullscreen mode Exit fullscreen mode
// order/domain/OrderCreatedEvent.java
package com.ecommerce.order.domain;

import com.ecommerce.shared.domain.DomainEvent;
import com.ecommerce.customer.domain.CustomerId;

public class OrderCreatedEvent extends DomainEvent {
    private final OrderId orderId;
    private final CustomerId customerId;

    public OrderCreatedEvent(OrderId orderId, CustomerId customerId) {
        super();
        this.orderId = orderId;
        this.customerId = customerId;
    }

    public OrderId getOrderId() {
        return orderId;
    }

    public CustomerId getCustomerId() {
        return customerId;
    }
}
Enter fullscreen mode Exit fullscreen mode
// order/domain/Order.java
package com.ecommerce.order.domain;

import com.ecommerce.shared.domain.AggregateRoot;
import com.ecommerce.customer.domain.CustomerId;
import com.ecommerce.product.domain.Money;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.math.BigDecimal;

public class Order extends AggregateRoot<OrderId> {
    private CustomerId customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private LocalDateTime orderDate;

    private Order(OrderId id, CustomerId customerId) {
        super(id);
        this.customerId = customerId;
        this.items = new ArrayList<>();
        this.status = OrderStatus.PENDING;
        this.orderDate = LocalDateTime.now();
    }

    public static Order create(CustomerId customerId) {
        Order order = new Order(OrderId.generate(), customerId);
        order.addDomainEvent(new OrderCreatedEvent(order.getId(), customerId));
        return order;
    }

    public static Order restore(OrderId id, CustomerId customerId, List<OrderItem> items, 
                               OrderStatus status, LocalDateTime orderDate) {
        Order order = new Order(id, customerId);
        order.items = new ArrayList<>(items);
        order.status = status;
        order.orderDate = orderDate;
        return order;
    }

    public void addItem(OrderItem item) {
        if (status != OrderStatus.PENDING) {
            throw new IllegalStateException("Cannot modify confirmed order");
        }
        items.add(item);
    }

    public void confirm() {
        if (items.isEmpty()) {
            throw new IllegalStateException("Cannot confirm empty order");
        }
        this.status = OrderStatus.CONFIRMED;
    }

    public Money getTotalAmount() {
        return items.stream()
                   .map(OrderItem::getTotalPrice)
                   .reduce(Money.usd(BigDecimal.ZERO), Money::add);
    }

    // Getters
    public CustomerId getCustomerId() { return customerId; }
    public List<OrderItem> getItems() { return new ArrayList<>(items); }
    public OrderStatus getStatus() { return status; }
    public LocalDateTime getOrderDate() { return orderDate; }
}
Enter fullscreen mode Exit fullscreen mode
// order/domain/OrderRepository.java
package com.ecommerce.order.domain;

import com.ecommerce.customer.domain.CustomerId;
import java.util.Optional;
import java.util.List;

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
    List<Order> findByCustomerId(CustomerId customerId);
}
Enter fullscreen mode Exit fullscreen mode

6. Application Layer

// order/application/dto/CreateOrderRequest.java
package com.ecommerce.order.application.dto;

import java.util.List;

public class CreateOrderRequest {
    private String customerId;
    private List<OrderItemRequest> items;

    public static class OrderItemRequest {
        private String productId;
        private int quantity;

        // Getters and setters
        public String getProductId() { return productId; }
        public void setProductId(String productId) { this.productId = productId; }
        public int getQuantity() { return quantity; }
        public void setQuantity(int quantity) { this.quantity = quantity; }
    }

    // Getters and setters
    public String getCustomerId() { return customerId; }
    public void setCustomerId(String customerId) { this.customerId = customerId; }
    public List<OrderItemRequest> getItems() { return items; }
    public void setItems(List<OrderItemRequest> items) { this.items = items; }
}
Enter fullscreen mode Exit fullscreen mode
// order/application/dto/OrderResponse.java
package com.ecommerce.order.application.dto;

import com.ecommerce.order.domain.Order;
import com.ecommerce.order.domain.OrderItem;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;

public class OrderResponse {
    private String id;
    private String customerId;
    private String status;
    private LocalDateTime orderDate;
    private BigDecimal totalAmount;
    private List<OrderItemResponse> items;

    public static class OrderItemResponse {
        private String productId;
        private String productName;
        private BigDecimal unitPrice;
        private int quantity;
        private BigDecimal totalPrice;

        public static OrderItemResponse from(OrderItem item) {
            OrderItemResponse response = new OrderItemResponse();
            response.productId = item.getProductId().getValue();
            response.productName = item.getProductName();
            response.unitPrice = item.getUnitPrice().getAmount();
            response.quantity = item.getQuantity();
            response.totalPrice = item.getTotalPrice().getAmount();
            return response;
        }

        // Getters
        public String getProductId() { return productId; }
        public String getProductName() { return productName; }
        public BigDecimal getUnitPrice() { return unitPrice; }
        public int getQuantity() { return quantity; }
        public BigDecimal getTotalPrice() { return totalPrice; }
    }

    public static OrderResponse from(Order order) {
        OrderResponse response = new OrderResponse();
        response.id = order.getId().getValue();
        response.customerId = order.getCustomerId().getValue();
        response.status = order.getStatus().name();
        response.orderDate = order.getOrderDate();
        response.totalAmount = order.getTotalAmount().getAmount();
        response.items = order.getItems().stream()
                              .map(OrderItemResponse::from)
                              .collect(Collectors.toList());
        return response;
    }

    // Getters
    public String getId() { return id; }
    public String getCustomerId() { return customerId; }
    public String getStatus() { return status; }
    public LocalDateTime getOrderDate() { return orderDate; }
    public BigDecimal getTotalAmount() { return totalAmount; }
    public List<OrderItemResponse> getItems() { return items; }
}
Enter fullscreen mode Exit fullscreen mode
// order/application/CreateOrderUseCase.java
package com.ecommerce.order.application;

import com.ecommerce.order.domain.*;
import com.ecommerce.customer.domain.*;
import com.ecommerce.product.domain.*;
import com.ecommerce.order.application.dto.CreateOrderRequest;
import com.ecommerce.shared.infrastructure.EventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional
public class CreateOrderUseCase {
    private final OrderRepository orderRepository;
    private final CustomerRepository customerRepository;
    private final ProductRepository productRepository;
    private final EventPublisher eventPublisher;

    public CreateOrderUseCase(OrderRepository orderRepository,
                             CustomerRepository customerRepository,
                             ProductRepository productRepository,
                             EventPublisher eventPublisher) {
        this.orderRepository = orderRepository;
        this.customerRepository = customerRepository;
        this.productRepository = productRepository;
        this.eventPublisher = eventPublisher;
    }

    public Order execute(CreateOrderRequest request) {
        // Validate customer exists
        CustomerId customerId = CustomerId.of(request.getCustomerId());
        customerRepository.findById(customerId)
            .orElseThrow(() -> new IllegalArgumentException("Customer not found"));

        // Create order
        Order order = Order.create(customerId);

        // Add items
        for (CreateOrderRequest.OrderItemRequest itemRequest : request.getItems()) {
            ProductId productId = ProductId.of(itemRequest.getProductId());
            Product product = productRepository.findById(productId)
                .orElseThrow(() -> new IllegalArgumentException("Product not found"));

            if (!product.isAvailable(itemRequest.getQuantity())) {
                throw new IllegalStateException("Insufficient stock for product: " + product.getName());
            }

            OrderItem item = new OrderItem(
                productId,
                product.getName(),
                product.getPrice(),
                itemRequest.getQuantity()
            );

            order.addItem(item);
            product.reduceStock(itemRequest.getQuantity());
            productRepository.save(product);
        }

        order.confirm();
        Order savedOrder = orderRepository.save(order);

        // Publish domain events
        savedOrder.getDomainEvents().forEach(eventPublisher::publish);
        savedOrder.clearDomainEvents();

        return savedOrder;
    }
}
Enter fullscreen mode Exit fullscreen mode

7. Infrastructure Layer

// customer/infrastructure/CustomerEntity.java
package com.ecommerce.customer.infrastructure;

import javax.persistence.*;

@Entity
@Table(name = "customers")
public class CustomerEntity {
    @Id
    private String id;

    @Column(nullable = false)
    private String name;

    @Column(nullable = false, unique = true)
    private String email;

    // Constructors
    public CustomerEntity() {}

    public CustomerEntity(String id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getters and setters
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}
Enter fullscreen mode Exit fullscreen mode
// customer/infrastructure/CustomerJpaRepository.java
package com.ecommerce.customer.infrastructure;

import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface CustomerJpaRepository extends JpaRepository<CustomerEntity, String> {
    Optional<CustomerEntity> findByEmail(String email);
}
Enter fullscreen mode Exit fullscreen mode
// customer/infrastructure/CustomerRepositoryImpl.java
package com.ecommerce.customer.infrastructure;

import com.ecommerce.customer.domain.*;
import org.springframework.stereotype.Repository;
import java.util.Optional;

@Repository
public class CustomerRepositoryImpl implements CustomerRepository {
    private final CustomerJpaRepository jpaRepository;

    public CustomerRepositoryImpl(CustomerJpaRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public Customer save(Customer customer) {
        CustomerEntity entity = new CustomerEntity(
            customer.getId().getValue(),
            customer.getName(),
            customer.getEmail().getValue()
        );
        jpaRepository.save(entity);
        return customer;
    }

    @Override
    public Optional<Customer> findById(CustomerId id) {
        return jpaRepository.findById(id.getValue())
            .map(this::toDomain);
    }

    @Override
    public Optional<Customer> findByEmail(Email email) {
        return jpaRepository.findByEmail(email.getValue())
            .map(this::toDomain);
    }

    private Customer toDomain(CustomerEntity entity) {
        return Customer.restore(
            CustomerId.of(entity.getId()),
            entity.getName(),
            Email.of(entity.getEmail())
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

8. Web Layer

// web/OrderController.java
package com.ecommerce.web;

import com.ecommerce.order.application.CreateOrderUseCase;
import com.ecommerce.order.application.dto.CreateOrderRequest;
import com.ecommerce.order.application.dto.OrderResponse;
import com.ecommerce.order.domain.*;
import com.ecommerce.customer.domain.CustomerId;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/orders")
public class OrderController {
    private final CreateOrderUseCase createOrderUseCase;
    private final OrderRepository orderRepository;

    public OrderController(CreateOrderUseCase createOrderUseCase,
                          OrderRepository orderRepository) {
        this.createOrderUseCase = createOrderUseCase;
        this.orderRepository = orderRepository;
    }

    @PostMapping
    public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
        try {
            Order order = createOrderUseCase.execute(request);
            return ResponseEntity.status(HttpStatus.CREATED)
                                .body(OrderResponse.from(order));
        } catch (IllegalArgumentException | IllegalStateException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @GetMapping("/{orderId}")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable String orderId) {
        return orderRepository.findById(OrderId.of(orderId))
            .map(order -> ResponseEntity.ok(OrderResponse.from(order)))
            .orElse(ResponseEntity.notFound().build());
    }

    @GetMapping("/customer/{customerId}")
    public ResponseEntity<List<OrderResponse>> getOrdersByCustomer(@PathVariable String customerId) {
        List<Order> orders = orderRepository.findByCustomerId(CustomerId.of(customerId));
        List<OrderResponse> responses = orders.stream()
            .map(OrderResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(responses);
    }
}
Enter fullscreen mode Exit fullscreen mode
// web/CustomerController.java
package com.ecommerce.web;

import com.ecommerce.customer.domain.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/customers")
public class CustomerController {
    private final CustomerRepository customerRepository;

    public CustomerController(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @PostMapping
    public ResponseEntity<CustomerResponse> createCustomer(@RequestBody CreateCustomerRequest request) {
        try {
            Customer customer = Customer.create(request.getName(), Email.of(request.getEmail()));
            Customer saved = customerRepository.save(customer);
            return ResponseEntity.status(HttpStatus.CREATED)
                                .body(CustomerResponse.from(saved));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @GetMapping("/{customerId}")
    public ResponseEntity<CustomerResponse> getCustomer(@PathVariable String customerId) {
        return customerRepository.findById(CustomerId.of(customerId))
            .map(customer -> ResponseEntity.ok(CustomerResponse.from(customer)))
            .orElse(ResponseEntity.notFound().build());
    }

    public static class CreateCustomerRequest {
        private String name;
        private String email;

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getEmail() { return email; }
        public void setEmail(String email) { this.email = email; }
    }

    public static class CustomerResponse {
        private String id;
        private String name;
        private String email;

        public static CustomerResponse from(Customer customer) {
            CustomerResponse response = new CustomerResponse();
            response.id = customer.getId().getValue();
            response.name = customer.getName();
            response.email = customer.getEmail().getValue();
            return response;
        }

        public String getId() { return id; }
        public String getName() { return name; }
        public String getEmail() { return email; }
    }
}
Enter fullscreen mode Exit fullscreen mode
// web/ProductController.java
package com.ecommerce.web;

import com.ecommerce.product.domain.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/products")
public class ProductController {
    private final ProductRepository productRepository;

    public ProductController(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @PostMapping
    public ResponseEntity<ProductResponse> createProduct(@RequestBody CreateProductRequest request) {
        try {
            Money price = Money.usd(request.getPrice());
            Product product = Product.create(
                request.getName(),
                request.getDescription(),
                price,
                request.getStockQuantity()
            );
            Product saved = productRepository.save(product);
            return ResponseEntity.status(HttpStatus.CREATED)
                                .body(ProductResponse.from(saved));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().build();
        }
    }

    @GetMapping
    public ResponseEntity<List<ProductResponse>> getAllProducts() {
        List<Product> products = productRepository.findAll();
        List<ProductResponse> responses = products.stream()
            .map(ProductResponse::from)
            .collect(Collectors.toList());
        return ResponseEntity.ok(responses);
    }

    @GetMapping("/{productId}")
    public ResponseEntity<ProductResponse> getProduct(@PathVariable String productId) {
        return productRepository.findById(ProductId.of(productId))
            .map(product -> ResponseEntity.ok(ProductResponse.from(product)))
            .orElse(ResponseEntity.notFound().build());
    }

    public static class CreateProductRequest {
        private String name;
        private String description;
        private BigDecimal price;
        private int stockQuantity;

        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
        public String getDescription() { return description; }
        public void setDescription(String description) { this.description = description; }
        public BigDecimal getPrice() { return price; }
        public void setPrice(BigDecimal price) { this.price = price; }
        public int getStockQuantity() { return stockQuantity; }
        public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
    }

    public static class ProductResponse {
        private String id;
        private String name;
        private String description;
        private BigDecimal price;
        private int stockQuantity;

        public static ProductResponse from(Product product) {
            ProductResponse response = new ProductResponse();
            response.id = product.getId().getValue();
            response.name = product.getName();
            response.description = product.getDescription();
            response.price = product.getPrice().getAmount();
            response.stockQuantity = product.getStockQuantity();
            return response;
        }

        public String getId() { return id; }
        public String getName() { return name; }
        public String getDescription() { return description; }
        public BigDecimal getPrice() { return price; }
        public int getStockQuantity() { return stockQuantity; }
    }
}
Enter fullscreen mode Exit fullscreen mode

9. Infrastructure Implementation - Product & Order

// product/infrastructure/ProductEntity.java
package com.ecommerce.product.infrastructure;

import javax.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "products")
public class ProductEntity {
    @Id
    private String id;

    @Column(nullable = false)
    private String name;

    @Column(columnDefinition = "TEXT")
    private String description;

    @Column(nullable = false, precision = 19, scale = 2)
    private BigDecimal price;

    @Column(nullable = false)
    private String currency;

    @Column(name = "stock_quantity", nullable = false)
    private int stockQuantity;

    // Constructors
    public ProductEntity() {}

    public ProductEntity(String id, String name, String description, 
                        BigDecimal price, String currency, int stockQuantity) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
        this.currency = currency;
        this.stockQuantity = stockQuantity;
    }

    // Getters and setters
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getDescription() { return description; }
    public void setDescription(String description) { this.description = description; }
    public BigDecimal getPrice() { return price; }
    public void setPrice(BigDecimal price) { this.price = price; }
    public String getCurrency() { return currency; }
    public void setCurrency(String currency) { this.currency = currency; }
    public int getStockQuantity() { return stockQuantity; }
    public void setStockQuantity(int stockQuantity) { this.stockQuantity = stockQuantity; }
}
Enter fullscreen mode Exit fullscreen mode
// product/infrastructure/ProductJpaRepository.java
package com.ecommerce.product.infrastructure;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductJpaRepository extends JpaRepository<ProductEntity, String> {
}
Enter fullscreen mode Exit fullscreen mode
// product/infrastructure/ProductRepositoryImpl.java
package com.ecommerce.product.infrastructure;

import com.ecommerce.product.domain.*;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;

@Repository
public class ProductRepositoryImpl implements ProductRepository {
    private final ProductJpaRepository jpaRepository;

    public ProductRepositoryImpl(ProductJpaRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public Product save(Product product) {
        ProductEntity entity = new ProductEntity(
            product.getId().getValue(),
            product.getName(),
            product.getDescription(),
            product.getPrice().getAmount(),
            product.getPrice().getCurrency(),
            product.getStockQuantity()
        );
        jpaRepository.save(entity);
        return product;
    }

    @Override
    public Optional<Product> findById(ProductId id) {
        return jpaRepository.findById(id.getValue())
            .map(this::toDomain);
    }

    @Override
    public List<Product> findAll() {
        return jpaRepository.findAll().stream()
            .map(this::toDomain)
            .collect(Collectors.toList());
    }

    private Product toDomain(ProductEntity entity) {
        return Product.restore(
            ProductId.of(entity.getId()),
            entity.getName(),
            entity.getDescription(),
            Money.of(entity.getPrice(), entity.getCurrency()),
            entity.getStockQuantity()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode
// order/infrastructure/OrderEntity.java
package com.ecommerce.order.infrastructure;

import com.ecommerce.order.domain.OrderStatus;
import javax.persistence.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "orders")
public class OrderEntity {
    @Id
    private String id;

    @Column(name = "customer_id", nullable = false)
    private String customerId;

    @Enumerated(EnumType.STRING)
    @Column(nullable = false)
    private OrderStatus status;

    @Column(name = "order_date", nullable = false)
    private LocalDateTime orderDate;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<OrderItemEntity> items = new ArrayList<>();

    // Constructors
    public OrderEntity() {}

    public OrderEntity(String id, String customerId, OrderStatus status, LocalDateTime orderDate) {
        this.id = id;
        this.customerId = customerId;
        this.status = status;
        this.orderDate = orderDate;
    }

    // Getters and setters
    public String getId() { return id; }
    public void setId(String id) { this.id = id; }
    public String getCustomerId() { return customerId; }
    public void setCustomerId(String customerId) { this.customerId = customerId; }
    public OrderStatus getStatus() { return status; }
    public void setStatus(OrderStatus status) { this.status = status; }
    public LocalDateTime getOrderDate() { return orderDate; }
    public void setOrderDate(LocalDateTime orderDate) { this.orderDate = orderDate; }
    public List<OrderItemEntity> getItems() { return items; }
    public void setItems(List<OrderItemEntity> items) { this.items = items; }
}
Enter fullscreen mode Exit fullscreen mode
// order/infrastructure/OrderItemEntity.java
package com.ecommerce.order.infrastructure;

import javax.persistence.*;
import java.math.BigDecimal;

@Entity
@Table(name = "order_items")
public class OrderItemEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private OrderEntity order;

    @Column(name = "product_id", nullable = false)
    private String productId;

    @Column(name = "product_name", nullable = false)
    private String productName;

    @Column(name = "unit_price", nullable = false, precision = 19, scale = 2)
    private BigDecimal unitPrice;

    @Column(nullable = false)
    private String currency;

    @Column(nullable = false)
    private int quantity;

    // Constructors
    public OrderItemEntity() {}

    public OrderItemEntity(OrderEntity order, String productId, String productName,
                          BigDecimal unitPrice, String currency, int quantity) {
        this.order = order;
        this.productId = productId;
        this.productName = productName;
        this.unitPrice = unitPrice;
        this.currency = currency;
        this.quantity = quantity;
    }

    // Getters and setters
    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }
    public OrderEntity getOrder() { return order; }
    public void setOrder(OrderEntity order) { this.order = order; }
    public String getProductId() { return productId; }
    public void setProductId(String productId) { this.productId = productId; }
    public String getProductName() { return productName; }
    public void setProductName(String productName) { this.productName = productName; }
    public BigDecimal getUnitPrice() { return unitPrice; }
    public void setUnitPrice(BigDecimal unitPrice) { this.unitPrice = unitPrice; }
    public String getCurrency() { return currency; }
    public void setCurrency(String currency) { this.currency = currency; }
    public int getQuantity() { return quantity; }
    public void setQuantity(int quantity) { this.quantity = quantity; }
}
Enter fullscreen mode Exit fullscreen mode
// order/infrastructure/OrderJpaRepository.java
package com.ecommerce.order.infrastructure;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;

public interface OrderJpaRepository extends JpaRepository<OrderEntity, String> {
    @Query("SELECT o FROM OrderEntity o WHERE o.customerId = :customerId")
    List<OrderEntity> findByCustomerId(@Param("customerId") String customerId);
}
Enter fullscreen mode Exit fullscreen mode
// order/infrastructure/OrderRepositoryImpl.java
package com.ecommerce.order.infrastructure;

import com.ecommerce.order.domain.*;
import com.ecommerce.customer.domain.CustomerId;
import com.ecommerce.product.domain.ProductId;
import com.ecommerce.product.domain.Money;
import org.springframework.stereotype.Repository;
import java.util.Optional;
import java.util.List;
import java.util.stream.Collectors;

@Repository
public class OrderRepositoryImpl implements OrderRepository {
    private final OrderJpaRepository jpaRepository;

    public OrderRepositoryImpl(OrderJpaRepository jpaRepository) {
        this.jpaRepository = jpaRepository;
    }

    @Override
    public Order save(Order order) {
        OrderEntity entity = new OrderEntity(
            order.getId().getValue(),
            order.getCustomerId().getValue(),
            order.getStatus(),
            order.getOrderDate()
        );

        List<OrderItemEntity> itemEntities = order.getItems().stream()
            .map(item -> new OrderItemEntity(
                entity,
                item.getProductId().getValue(),
                item.getProductName(),
                item.getUnitPrice().getAmount(),
                item.getUnitPrice().getCurrency(),
                item.getQuantity()
            ))
            .collect(Collectors.toList());

        entity.setItems(itemEntities);
        jpaRepository.save(entity);
        return order;
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return jpaRepository.findById(id.getValue())
            .map(this::toDomain);
    }

    @Override
    public List<Order> findByCustomerId(CustomerId customerId) {
        return jpaRepository.findByCustomerId(customerId.getValue()).stream()
            .map(this::toDomain)
            .collect(Collectors.toList());
    }

    private Order toDomain(OrderEntity entity) {
        List<OrderItem> items = entity.getItems().stream()
            .map(itemEntity -> new OrderItem(
                ProductId.of(itemEntity.getProductId()),
                itemEntity.getProductName(),
                Money.of(itemEntity.getUnitPrice(), itemEntity.getCurrency()),
                itemEntity.getQuantity()
            ))
            .collect(Collectors.toList());

        return Order.restore(
            OrderId.of(entity.getId()),
            CustomerId.of(entity.getCustomerId()),
            items,
            entity.getStatus(),
            entity.getOrderDate()
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

10. Configuration Files

# application.yml
spring:
  application:
    name: ecommerce-ddd

  datasource:
    url: jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password: password

  jpa:
    database-platform: org.hibernate.dialect.H2Dialect
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true

  h2:
    console:
      enabled: true
      path: /h2-console

server:
  port: 8080

logging:
  level:
    com.ecommerce: DEBUG
    org.hibernate.SQL: DEBUG
Enter fullscreen mode Exit fullscreen mode
-- data.sql
-- Sample data for testing
INSERT INTO customers (id, name, email) VALUES 
('cust-001', 'John Doe', 'john.doe@example.com'),
('cust-002', 'Jane Smith', 'jane.smith@example.com');

INSERT INTO products (id, name, description, price, currency, stock_quantity) VALUES 
('prod-001', 'Laptop', 'High-performance laptop', 999.99, 'USD', 10),
('prod-002', 'Mouse', 'Wireless mouse', 29.99, 'USD', 50),
('prod-003', 'Keyboard', 'Mechanical keyboard', 149.99, 'USD', 25);
Enter fullscreen mode Exit fullscreen mode

11. Maven Dependencies (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>ecommerce-ddd</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>E-Commerce DDD</name>
    <description>E-Commerce system using Domain Driven Design</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/>
    </parent>

    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
Enter fullscreen mode Exit fullscreen mode

12. Unit Tests

// test/java/com/ecommerce/order/domain/OrderTest.java
package com.ecommerce.order.domain;

import com.ecommerce.customer.domain.CustomerId;
import com.ecommerce.product.domain.ProductId;
import com.ecommerce.product.domain.Money;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import static org.junit.jupiter.api.Assertions.*;

class OrderTest {

    @Test
    void shouldCreateEmptyOrder() {
        CustomerId customerId = CustomerId.generate();
        Order order = Order.create(customerId);

        assertNotNull(order.getId());
        assertEquals(customerId, order.getCustomerId());
        assertEquals(OrderStatus.PENDING, order.getStatus());
        assertTrue(order.getItems().isEmpty());
        assertNotNull(order.getOrderDate());
    }

    @Test
    void shouldAddItemToOrder() {
        Order order = Order.create(CustomerId.generate());
        OrderItem item = new OrderItem(
            ProductId.generate(),
            "Test Product",
            Money.usd(new BigDecimal("99.99")),
            2
        );

        order.addItem(item);

        assertEquals(1, order.getItems().size());
        assertEquals(item, order.getItems().get(0));
    }

    @Test
    void shouldCalculateTotalAmount() {
        Order order = Order.create(CustomerId.generate());

        OrderItem item1 = new OrderItem(
            ProductId.generate(),
            "Product 1",
            Money.usd(new BigDecimal("99.99")),
            2
        );

        OrderItem item2 = new OrderItem(
            ProductId.generate(),
            "Product 2",
            Money.usd(new BigDecimal("49.99")),
            1
        );

        order.addItem(item1);
        order.addItem(item2);

        Money total = order.getTotalAmount();
        assertEquals(new BigDecimal("249.97"), total.getAmount());
    }

    @Test
    void shouldConfirmOrderWithItems() {
        Order order = Order.create(CustomerId.generate());
        OrderItem item = new OrderItem(
            ProductId.generate(),
            "Test Product",
            Money.usd(new BigDecimal("99.99")),
            1
        );

        order.addItem(item);
        order.confirm();

        assertEquals(OrderStatus.CONFIRMED, order.getStatus());
    }

    @Test
    void shouldNotConfirmEmptyOrder() {
        Order order = Order.create(CustomerId.generate());

        assertThrows(IllegalStateException.class, order::confirm);
    }

    @Test
    void shouldNotAddItemToConfirmedOrder() {
        Order order = Order.create(CustomerId.generate());
        OrderItem item1 = new OrderItem(
            ProductId.generate(),
            "Product 1",
            Money.usd(new BigDecimal("99.99")),
            1
        );

        order.addItem(item1);
        order.confirm();

        OrderItem item2 = new OrderItem(
            ProductId.generate(),
            "Product 2",
            Money.usd(new BigDecimal("49.99")),
            1
        );

        assertThrows(IllegalStateException.class, () -> order.addItem(item2));
    }
}
Enter fullscreen mode Exit fullscreen mode

API Usage Examples

1. Create Customer

POST /api/customers
{
    "name": "John Doe",
    "email": "john.doe@example.com"
}
Enter fullscreen mode Exit fullscreen mode

2. Create Product

POST /api/products
{
    "name": "Laptop",
    "description": "High-performance laptop",
    "price": 999.99,
    "stockQuantity": 10
}
Enter fullscreen mode Exit fullscreen mode

3. Create Order

POST /api/orders
{
    "customerId": "cust-001",
    "items": [
        {
            "productId": "prod-001",
            "quantity": 1
        },
        {
            "productId": "prod-002", 
            "quantity": 2
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

4. Get Order

GET /api/orders/{orderId}
Enter fullscreen mode Exit fullscreen mode

I've completed the comprehensive Spring Boot DDD project! This e-commerce order management system demonstrates all the key principles of Domain Driven Design:

Key Features Implemented:

🏗️ DDD Architecture:

  • Domain Layer: Rich domain models (Customer, Product, Order)
  • Application Layer: Use cases and DTOs
  • Infrastructure Layer: JPA repositories and entities
  • Web Layer: REST controllers

🎯 Core DDD Concepts:

  • Aggregates: Order, Customer, Product as aggregate roots
  • Value Objects: Money, Email, CustomerId with immutability
  • Domain Events: OrderCreatedEvent for decoupled communication
  • Repositories: Clean abstraction over data access
  • Ubiquitous Language: Business-focused terminology throughout

⚡ Business Logic:

  • Customer management with email validation
  • Product inventory tracking
  • Order creation with stock validation
  • Domain event publishing
  • Transaction management

🚀 Technical Stack:

  • Spring Boot 2.7.0
  • Spring Data JPA
  • H2 Database (in-memory)
  • JUnit 5 for testing
  • Maven for dependency management

📊 API Endpoints:

  • Customer CRUD operations
  • Product management
  • Order creation and retrieval
  • Comprehensive error handling

The project follows clean architecture principles with proper separation of concerns. Each domain is isolated with its own repository, and business rules are encapsulated within domain objects. The application includes comprehensive unit tests and sample data for quick testing.

You can run this application with mvn spring-boot:run and access the H2 console at http://localhost:8080/h2-console for database inspection.

Top comments (0)