Recently, while reviewing the talk The Modular Monolith – a Practical Alternative to Microservices by Victor Rentea, I found myself pondering a seemingly simple question: where should we implement the logic for GET /customers/{id}/orders ?

In this article, I’ll walk you through three possible design approaches, the trade-offs of each, and why the final choice not only respects encapsulation but also scales well within a monolithic architecture. The discussion was shaped by insights from Victor himself, and it helped clarify a lot about practical modular design.

Why we’re interested in this endpoint? Link to heading

Some of view, will say, “Adrien, why focus so much on this one endpoint?”. Give me 1 minute to show how many questions this endpoint raises.

Coding the endpoint GET /order/{id} is pretty straightforward. Victor Rentea give us the code

  • The owner of the endpoint is OrderRestApi – the API layer of the Order module.
  • The business logic is delegated to OrderService.
  • OrderService then calls OrderRepository to access the data.

Now, please answers to this question “How get all orders of a specific user ?”

  • Where does the API layer belongs ? OrderRestAPI or CustomerRestAPI
  • Where does the business logic really belong? OrderService or CustomerService
  • How do we maintain clean boundaries between modules?

How get all orders of a specific user ? Link to heading

One possible approach is to design a GET /customers/{id}/orders endpoint. In this section, we’ll explore three different implementation strategies, each with its own set of advantages and trade-offs.

Hypothesis 1: Return Orders from CustomerService Link to heading

public class Customer {
    List<Order> orders;
}
public class CustomerRestAPI {
    private final CustomerService customerService;

    @GetMapping("customers/{id}/orders")
    public ResponseEntity<List<Order>> getOrdersByCustomerId(@PathVariable Integer id) {
        List<Order> orders = customerService.getOrdersByCustomerId(id);
        return ResponseEntity.ok(orders);
    }
}

public class CustomerService {
    public List<Order> getOrderByCustomerId(Integer customerId) {
        return customerRepository.findById(customerId).getOrders();
    }
}

However, this approach breaks encapsulation, since the customer package would now depend on Domain Entity of the order package.

image

Hypothesis 2: Implement getOrderByCustomerId in OrderService Link to heading

public class Customer { 
    // List<Order> orders; no longer reference Order 
}

public class Order {  private Integer customerId; } 
public class OrderService {
    public List<Order> getOrderByCustomerId(Integer customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
}

This design is cleaner in terms of boundaries, as the Order package only needs the customerId and doesn’t depend on the Customer package.

However, the CustomerRestApi would now need to depend on both CustomerService and OrderService

@RestController
public class CustomerRestApi {
    private final CustomerService customerService;
    private final OrderService orderService;

    @GetMapping("customers/{customerId}")
    public Customer getCustomerById(@PathVariable Integer customerId) {
        return customerService.findById(customerId);
    }

    @GetMapping("customers/{customerId}/orders")
    public List<Order> getOrdersByCustomerId(@PathVariable Integer customerId) {
        return orderService.getOrderByCustomerId(customerId);
    }
}

In this architecture, the customer package depends on the order package—but only through its public API layer (aka OrderService), not the domain entities directly.

image

Hypothesis 3: Two Bounded Contexts — Only IDs Are Shared Link to heading

This is aligned with your domain-driven design approach. Order only knows the customerId (see line 25), and similarly, Customer can have a List<Integer> orderIds. In this case, the CustomerService could return the order IDs

class Customer {
    List<Integer> ordersId
}
@RestController
public class CustomerRestApi {
    private final CustomerService customerService;

    @GetMapping("customers/{customerId}/orders")
    public List<Integer> getOrdersByCustomerId(@PathVariable Integer customerId) {
        return customerService.getOrderByCustomerId(customerId);
    }
}

public class CustomerService {
    public List<Integer> getOrderByCustomerId(Integer customerId) {
        return customerRepository.findById(customerId).getOrderIds();
    }
}

With this approach, the packages are now completely decoupled. However, this comes at the cost of additional complexity on the client side. The frontend must iterate over each orderId to fetch the corresponding order details individually, which can result in multiple network calls

image

Better approach Link to heading

Heuristic Link to heading

As a general rule, domain entities should not directly reference each other1. Instead:

  • The Order entity should hold a customerId.
  • The Customer entity could maintain a List<Integer> orderIds.

However, in this specific use case, storing orderIds in the Customer domain doesn’t provide meaningful value—it introduces unnecessary duplication and potential for inconsistency.

So how design GET /customers/{id}/orders ? Link to heading

A cleaner and more decoupled alternative is to invert the query. Rather than asking the Customer module for its orders, let the Order module expose a query endpoint:

GET /order?customerId=123

This shifts the responsibility to the Order bounded context, which owns the data and logic related to orders, and is therefore the most appropriate place to perform filtering by customerId.

@RestController
@RequestMapping("/orders")
public class OrderRestApi {
    private final OrderService orderService

    @GetMapping
    public List<Order> getOrdersByCustomerId(@RequestParam Integer customerId) {
        return orderService.getOrdersByCustomerId(customerId);
    }
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;

    public List<Order> getOrdersByCustomerId(Integer customerId) {
        return orderRepository.findByCustomerId(customerId);
    }
}

  1. https://enterprisecraftsmanship.com/posts/link-to-an-aggregate-reference-or-id/ I agree with this article, but in your case with JPA and Domain Entity linked (maybe considered a “bad practice”) ↩︎