I once had to track down a discount that was applied to the wrong price. A discount in that system has many possible configurations: a percentage or an absolute amount, a minimum price, a minimum number of items in the basket, a time range. When a final price came out wrong, the cause could be the product configuration, the sum of the items in the basket, or the discount itself. Checking each possibility by hand could take hours.
What worked was cutting the problem in half, then in half again. This is the dichotomy method: binary search applied to the execution path. You check the state in the middle, discard the half that behaves correctly, and repeat until one method is left.
Start from the boundaries
The bug is rarely where the symptom appears. For the discount, I started by printing two things: the data loaded from the database at the beginning of the request, and the final price returned at the end. If the input is right and the output is wrong, the bug is in the code between them. If the input is already wrong, it’s a data problem.
With several services involved, the same idea applies one level up. Check that the endpoint receives the expected payload, query the database directly to compare its state with what you expect, and follow the correlation IDs to see whether a downstream service fails silently or returns bad data. An endpoint returning a 500 can come from the handler, a downstream service, or a malformed record.
Split the execution path
Say a request goes through ten methods, M1 to M10. Don’t start at M1. Check the output of M5.
- If the output of M5 is correct, the bug is between M6 and M10.
- If the output of M5 is wrong, the bug is between M1 and M5. M5 itself is still a suspect.
Each check removes half of the remaining code, so ten methods take at most four checks.
On the discount bug, I identified the two or three main methods of the price calculation and added prints after each of them: the item prices once loaded, and the discounts retrieved from the database. Every run showed which method returned an unexpected output, and I added more prints inside that one until a single method was left.
public class PriceService { private static final Logger log = LoggerFactory.getLogger(PriceService.class); public BigDecimal computeFinalPrice(Basket basket) { List<Discount> discounts = discountRepository.findApplicable(basket.getCustomerId(), Instant.now()); BigDecimal subtotal = pricingService.computeSubtotal(basket.getItems()); log.debug("subtotal={} discounts={}", subtotal, discounts); BigDecimal discounted = discountCalculator.apply(subtotal, basket, discounts); return taxService.applyTaxes(discounted); }}
If the subtotal and the discounts are correct at the log line, the product configuration and the discount lookup are cleared, and the bug is in apply or after it.
Reproduce it locally
The game changer for me was connecting the application running on my machine to the production database, with a read-only user. When the purchase with the wrong price is identified, reproducing it means picking the same items and the same discounts. Then the whole investigation happens in the IDE: add a print, rerun, read the output, with no deployment in between.
Use a read-only user. A debugging session is exactly when you run something you didn’t intend to.
It gets harder when a client reports something vague, like “I had a problem with some products”. There’s no purchase to replay, so you have to test several scenarios until one of them fails.
Match the production feature flags in your local configuration. If the bug is inside your own service, mock the dependencies with Mockito or WireMock so network issues and rate limits don’t get in the way. And if it fails in production but not locally with the same data, look at the infrastructure, the environment variables, or the data volume before the logic.
Sometimes running locally isn’t possible, for security reasons or because the setup is too complex. Then add debug logs on the deployed server. It works, but every split costs a deployment, and the investigation takes a lot longer.
When the method failed me
Dichotomy assumes you’re testing with the real input. Once I started from a payload and an expected value, narrowed down step by step, and ended up with every method returning the right result. The payload I was using wasn’t the one production had received.
Before splitting anything, confirm the input matches production exactly: the JSON payload, the headers, and the database state at the time of the request.


Leave a Reply