Optivem Journal

Optivem Journal

Clean Architecture

Clean Architecture: One Class Per Use Case Is NOT Enough

Code Example

Valentina Jemuović's avatar
Valentina Jemuović
Sep 10, 2026
∙ Paid

ATDD in Legacy Code: catch regression bugs before production.
Hands-on work with your teams. Limited spaces for 2027.

Let's talk


📅 Join our next live course: ATDD, Clean Architecture, Pipelines
Ealy bird: €100 off with code EARLYBIRD100


You already know a fat service class is a bad place for all your business logic.

So you split it up.

One class per use case, like in Uncle Bob's example:

public class PlaceOrder {
    public PlaceOrderResponse placeOrder(PlaceOrderRequest request) { ... }
}

public class ViewOrderDetails {
    public ViewOrderDetailsResponse getOrder(String orderNumber) { ... }
}

public class CancelOrder {
    public void cancelOrder(String orderNumber) { ... }
}

public class PublishCoupon {
    public Coupon createCoupon(String couponCode, BigDecimal discountRate,
                                Instant validFrom, Instant validTo, Integer usageLimit) { ... }
}

public class BrowseCoupons {
    public List<Coupon> getAllCoupons() { ... }
}

Looks better.

But splitting the service into separate classes didn’t fix everything.

Here’s what’s still wrong:

❌ Entities leak across the boundary — but only sometimes

PublishCoupon returns a Coupon.

BrowseCoupons returns List<Coupon>.

But PlaceOrder and ViewOrderDetails return response DTOs.

So... can entities cross the use case boundary or not?

You can’t tell. It depends on which class you’re looking at.

❌ A bug is hiding in PublishCoupon

If you accidently swap validFrom and validTo — it still compiles, because they’re the same type.

But now your coupon has the wrong validity period — it’s backwards.

CouponController already has a PublishCouponRequest with all the values. Instead of passing that request to PublishCoupon, it pulls the values out and passes them one by one.

That's how you end up with a bug like this.

❌ Expected outcomes are thrown, not declared

Order not found, order can’t be delivered — are thrown as exceptions instead of declared.

These aren't bugs. They're things that can happen when the use case runs.

But they’re in a shared exception handler that has grown a case for every use case's failures.

The method says “I return an order.” It doesn't tell you that “order not found” or “order can't be delivered” can happen.

❌ Calling each use case is different

PlaceOrder takes a request object, ViewOrderDetails/CancelOrder take a raw String, PublishCoupon takes five loose parameters, BrowseCoupons takes nothing.

There’s no obvious answer to: “How do I call a use case?”

❌ The class says one thing, the method says another

ViewOrderDetails.getOrder()

The class says “view the order details”, but the method says “get the order.”

Same problem here: PublishCoupon.createCoupon()

The class says “publish a coupon”, but the method says “create a coupon”.

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2026 Valentina Jemuović, Optivem · Privacy ∙ Terms ∙ Collection notice
Start your SubstackGet the app
Substack is the home for great culture