For complex topics, I stop using Spring Services

Let’s say I have a complex service to put in place. Let’s say a critical system for price calculation. The prices are always critical. As it’s critical, it means that I must cover it with a lot of unit tests.

Let’s see an example of how to do it with a regular Spring Service.

@RequiredArgsConstructor
@Service
public class PriceService {
    private final ProductRepository productRepository;
    private final DiscountRepository discountRepository;
    ...
    private final UserService userService;
    ...

    public BigDecimal calculatePrice(List<Product> products, User user) {...}
    
    public BigDecimal calculateSinglePrice(Product product, List<Discount> discount) {...}

    public List<Discount> findAppliedDiscounts(List<Product> products, User user) {...}

    private List<Discount> fetchDiscounts(User user) {...}

    private boolean applyDiscountToProduct(Product p, Discount d) {...}
    private boolean applyDiscountToGroupedProducts(...) {...}
    ...
}

The first thing done is injecting all the dependent services and repositories.

With Spring, this is so easy. Still, this will be harder when writing the unit tests, because I will need to mock all those services.

For the repositories, I can also mock them or inject some data in the database and use it. Still, this is more advanced tests which need more maintenance.

Finally, I end up my service with 1 or 2 public methods to compute the prices. But in the case I’ve written, I need the total price, the unit prices, and the applied discounts. When computing the total price, I must repeat the unit prices operations. I must also check again which discounts to apply.

As you may notice, that’s a lot of duplication.

Why don’t use an Object Oriented solution?

What are the benefits?

The first benefit is to move all the dependencies outside. This will make the unit tests easier to write.

The second benefit is that I won’t need to compute again the unit prices to get the final price. I can store everything in local variables of the class.

Another benefit is that the methods will have fewer or no arguments, as I store everything in local variables of the object.

How to do so?

The first thing I need is a context with all the input values.

public record InputPricesContext(User user, List<Product> products, List<Discount> discounts) {}

What’s in this context? All the values I need to compute all the prices. I will put the necessary to avoid any call to the repositories or external services. I fetch them all when building the context.

Having everything in this context, allows me to write simpler unit tests, without the need to create mocks.

Now, when I instantiate my Handler (I call it handler to differentiate it from the Spring Services) I pass the context as the only argument.

public class PriceHandler {
    private final InputPricesContext context;
    public PriceHandler(InputPricesContext context) {
        this.context = context;
    }
    ...
}

Let’s continue with the public methods. Now, as all the needed data is already inside my Handler, the public methods don’t need arguments.

var handler = new PriceHandler(inputPricesContext);
var totalPrice = handler.computeTotalPrice();
var discounts = handler.findAppliedDiscounts();
var unitPrices = handler.computeUnitPrices();

This becomes a lot easier to read and use.

Let’s see now inside my Handler what changed.

@RequiredArgsConstructor
public class PriceHandler {
    private final InputPricesContext context;
    private BigDecimal totalPrice = BigDecimal.ZERO;
    private List<BigDecimal> unitPrices;
    private List<Discount> appliedDiscounts;

    public BigDecimal calculatePrice(List<Product> products, User user) {
        for (Product product in this.products) {
            ...
        }
        return this.totalPrice;
    }
    
    public BigDecimal calculateSinglePrice() {
        this.
    }

    public List<Discount> findAppliedDiscounts(List<Product> products, User user) {
        ...
        return this.appliedDiscounts;
    }

    private void fetchDiscounts(User user) {...}

    private boolean applyDiscountToProduct(Product p, Discount d) {...}

    private boolean applyDiscountToGroupedProducts(...) {...}
    ...
}

What if I call computeUnitPrices before computeTotalPrice? Will this cause a NullPointerException or compute the prices twice?

A solution for that is to have a single public method which returns all I need into an output context.

public record OutputPricesContext (BigDecimal totalPrice, List<BigDecimal> unitPrices, List<Discount> appliedDiscounts) {}
public OutputPricesContext computePrices();

If I have only one public method, there is no chance the developers use the Handler incorrectly.

I use this pattern a lot when I have a complex system to implement and test. I use it when I see the benefits of the Object Oriented over the Spring Services.

The thing is, not because I’m developing a Spring Boot application, I must always use the Spring Services.

If you want to learn more about good quality code, make sure to follow me on Youtube.


Never Miss Another Tech Innovation

Concrete insights and actionable resources delivered straight to your inbox to boost your developer career.

My New ebook, Best Practices To Create A Backend With Spring Boot 3, is available now.

Best practices to create a backend with Spring Boot 3

Leave a comment

Discover more from The Dev World - Sergio Lema

Subscribe now to keep reading and get access to the full archive.

Continue reading