ATDD in Legacy Code: catch regression bugs before production.
Hands-on work with your teams. Limited spaces for 2027.
📅 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”.

