0

I want to create an example application where we use a strict domain-driven design and layering (controller, service, repository). Most notably, we have a clear distinction between the domain and the persistence layer. See my example below:

Domain

public class Customer {
    private Long id;
    private String name;
}

public class CustomerOrder {
    private Long id;
    private String productName;
    private int quantity;
    private Customer customer;
}

Entity

@Entity
public class CustomerEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
}

@Entity
public class CustomerOrderEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String productName;
    private int quantity;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "customer_id")
    private CustomerEntity customer;
}

DTO

@Data
public class CustomerOrderRequestDTO {
    private Long customerId;
    private String productName;
    private int quantity;
}

@Data
public class CustomerOrderResponseDTO {
    private Long orderId;
    private String productName;
    private int quantity;
    private Long customerId;
    private String customerName;
}

Controller layer

@RestController
@RequestMapping("/api/orders")
@AllArgsConstructor
public class CustomerOrderController {

    private final CustomerOrderService customerOrderService;

    @PostMapping
    public ResponseEntity<CustomerOrderResponseDTO> createOrder(@RequestBody CustomerOrderRequestDTO requestDTO) {
        CustomerOrderResponseDTO responseDTO = customerOrderService.createOrder(requestDTO);
        return ResponseEntity.ok(responseDTO);
    }
}

Service layer

@Service
@AllArgsConstructor
public class CustomerOrderService {

    private final CustomerRepository customerRepository;
    private final CustomerOrderRepository customerOrderRepository;
    private final Mapper mapper;

     public CustomerOrderResponseDTO createOrder(CustomerOrderRequestDTO requestDTO) {
        
        CustomerEntity customerEntity = customerRepository.findById(requestDTO.getCustomerId())
                .orElseThrow(() -> new IllegalArgumentException("Customer not found"));

        Customer customer = mapper.map(customerEntity);
        CustomerOrder customerOrder = mapper.map(requestDTO, customer);

        //maybe do some validation on the domain instances

        CustomerOrderEntity customerOrderEntity = mapper.map(customerOrder);
        customerOrderRepository.save(customerOrderEntity);

        CustomerOrderResponseDTO responseDTO = mapper.map(customerOrder);

        return responseDTO;
    }
}

Repository layer

Just some default JpaRepostories.

Question

In this example, the service layer does a lot of mappings. Is this layer indeed responsible for all those mappings? If not, how would you change the application?

2 Answers 2

3

No it shouldn't be. indeed you shouldn't have a service layer in DDD as your logic is in the Domain.

How to fix:

  1. Delete the DTO, Entity and Service classes.
  2. Make a Repository in your Data layer that handles the mapping of a domain object to a database and back. any mapping classes or entities you need for your DRMs should be internal/private to this library/class
  3. Use the same framework that currently maps incoming json to DTOs to map incoming json to Domain Models
  4. Change the IDs to GUIDs so you can give new objects unique IDs without a database connection.

Your controller then becomes:

@RestController
@RequestMapping("/api/orders")
@AllArgsConstructor
public class CustomerOrderController {

    private final CustomerOrderRepository repo;

    @PostMapping
    public void SaveOrder(@RequestBody CustomerOrder order) {
        repo.Upsert(order)
    }
}

If you have actual logic rather than just CRUD to perform you have a choice.

  1. Add the logic to the Domain Object. ie CustomerOrder.Invoice() If you do a lot of this you might find you need DTOs again and have more than one Domain Model, ie Warehouse.CustomerOrder, ECommerceWebsite.CustomerOrder. But it's more OOP and DDD
  2. Add the logic to a Domain Service ie InvoiceCreator.CreateInvoiceFor(CustomerOrder order) keep your Domain Models anemic data structs. good for reuse
4
  • Isn't it a problem that the domain model is now exposed through the API, making it very unstable?
    – Gman
    Commented Aug 12 at 18:05
  • 1
    how does that make it "unstable"?
    – Ewan
    Commented Aug 12 at 20:51
  • I've had problems mapping incoming requests to domain models. It works OK for simple use cases, but things run afoul when you need to validate user input or limit the information that can be manipulated in the request. Honestly, I absolutely do not recommend this for security reasons. Mapping from database to domain model is my preferred approach, though. Commented Aug 13 at 12:52
  • It pushes logic to the application rather than the API, so if you have a web app you might want a BFF to do View Model -> Domain Model. But you should be able to pass your domain objects around without concern.
    – Ewan
    Commented Aug 13 at 19:36
1

Personally, I would put the mapping from model to API Dto in the Controller and I would put the mapping from database entity to model object into the EntityRepository. This way the controller is responsible for the complete offering of the API (specification of endpoints and input + output format), the repositories handle the loading of data from the database providing it in a model form and the service layer only holds the business logic operating on model objects and spitting them out. This way, you could easily change how to load your data or how to access the business logic (e.g. an alternative controller could offer GRPC and different API level dto representation).

However, the Hibernate based Repositories make this split a bit hard to achieve, which is one of the reasons I would typically not recommend to use Hibernate and co, but rather build your data repository classes yourself (e.g. using jooq).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.