Katkat

Code Review: A Ride Fare Method That Charges a Card Then Logs the Trip

MercorSoftware Engineer · Technical Screen

Last updated

Review a Java method that charges a rider's card through an external processor and then writes a trip record, and explain what goes wrong if the caller fails mid-call and how to fix it.

A ride-hailing backend has this Java service, which captures a rider's fare through an external card processor and then logs the completed trip. The interviewer asks: what happens if the process calling this method crashes or times out partway through, and how would you change the code? This is a discussion round — no new code needs to compile, but be specific about the fix.

import java.util.Map;

public class FareCaptureService {
    private final CardProcessor processor;
    private final Database db;

    public Map<String, String> captureFare(String riderId, int fareCents) {
        String captureId = processor.capture(riderId, fareCents);
        db.insert("trips", Map.of(
            "rider_id", riderId,
            "fare_cents", fareCents,
            "capture_id", captureId
        ));
        return Map.of("status", "ok", "capture_id", captureId);
    }
}

Assume processor.capture makes a network call that moves money and returns a processor-assigned capture ID, throwing on failure, and db.insert writes a single row with no further side effects.

Clarifying Questions

  • Is "the caller" the dispatcher service invoking captureFare (which may retry on timeout), or captureFare's own connection to the card processor?
  • Can the card processor accept an idempotency key, or let us look up an existing capture by a reference value we provide?
  • What should the dispatcher be told when the outcome of a capture attempt is genuinely unknown (request sent, response never received)?
  • Can the dispatcher attach a unique ID to each fare-capture attempt before calling this method?

What a Strong Answer Covers

  • Every point between and around the two calls where a crash or network failure leaves the system in an inconsistent state (money moved but untracked, money moved twice, or genuinely unknown outcome).
  • Why simply wrapping both calls in a database transaction does not make them atomic — the processor call isn't part of that transaction and can't be rolled back by it.
  • An idempotency key generated once per fare-capture attempt, enforced with a uniqueness constraint in the database and passed through to the processor so a retried call is recognized as the same attempt rather than a new charge.
  • What the method should return (or raise) when the capture succeeded but the database write failed, versus when the capture call itself timed out with an unknown outcome.
  • A reconciliation path: a background job that compares the processor's record of captures against the trips table and resolves rows left in an unrecorded or unknown state.

Hints

Hint 1

Draw a timeline with three points: before processor.capture, between the two calls, and after db.insert — write down what state the processor and the database are in at each point if the process dies right there.

Hint 2

A caller that times out waiting for captureFare will very likely retry with the same inputs — decide what would let the system recognize that retry as the same fare rather than charging the rider again.

Hint 3

A transaction around db.insert alone can't undo processor.capture if it already succeeded — atomicity across the two systems needs a different mechanism.

Hint 4

Consider what the method should return when it genuinely doesn't know whether the charge succeeded (e.g. the processor call times out) — 'ok' and 'failed' aren't the only two honest answers.

Follow-up questions

  • How would you design the idempotency key itself — client-generated UUID, or server-generated and returned before the first attempt?
  • What would you change if the card processor did not support idempotency keys at all?
  • How does this design change if captureFare can be called concurrently for the same rider from two different requests?
  • How would you test the retry-after-timeout path without a real flaky payment processor?