LLD Dojo

State

Core. Expect to meet this one, and expect to be asked for it by name.

Start with the problem

A ride-share trip moves through five stages: requested, matched, driver arrived, in progress, completed. The obvious way to encode what happens on each event is a switch inside one method.

Trip handle(Trip trip, Event event) {
    switch (trip.state()) {
        case REQUESTED -> {
            if (event.kind() == Kind.MATCH) return trip.withState(State.MATCHED);
        }
        case MATCHED -> {
            if (event.kind() == Kind.ARRIVE) return trip.withState(State.DRIVER_ARRIVED);
        }
        // ...
    }
    throw new IllegalStateException("no transition for " + trip.state() + " on " + event.kind());
}

For five stages and a handful of events, this compiles, reads top to bottom, and a reviewer can check every case against the requirement.

Watch where it goes

A market asks for a shortcut: a driver who forgot to tap arrive should still be able to start the trip. That is one more branch inside one existing case. Then a new event arrives entirely, a rider cancelling mid-trip, and it has to be wired into every case that might see it, not one alone.

The switch still works. What changes is who has to reopen it. handle is the one method every part of the system funnels through. A market-specific rule and a brand-new event both land as edits to that same block of code. The rules for what is legal end up mixed in with the code that carries them out.

The move

Pull the rules themselves out as data: a list of rows, each one a plain function from a state and an event to the next state.

public record Edge(TripState from, EventKind event, TripState to) {}

public final class Transitions {
    public static List<Edge> standard() {
        return List.of(
            new Edge(TripState.REQUESTED, EventKind.MATCH, TripState.MATCHED),
            new Edge(TripState.MATCHED, EventKind.ARRIVE, TripState.DRIVER_ARRIVED),
            new Edge(TripState.DRIVER_ARRIVED, EventKind.START, TripState.IN_PROGRESS),
            new Edge(TripState.IN_PROGRESS, EventKind.COMPLETE, TripState.COMPLETED));
    }
}

This is corpus/trip-state-machine's shape. handle no longer contains the rules; it looks one up in the table and applies it. A market's shortcut, letting ARRIVE be skipped, is one new Edge. One of the three measured lines it costs is a comma. A new event, a mid-trip cancellation, still has to be wired into every place that watches for events. That costs more: eight lines, spread across four files, because a new kind of thing touches more than a new rule about an existing one.

What modern Java changes here

TripState as a plain enum and Edge as a record do the whole job here. Nothing in corpus/trip-state-machine needs a State interface with a handle method each state subclass overrides, which is the shape the original catalogue describes. That subclass version earns its place once a state needs its own behavior beyond "what comes next" — an EntryAction that runs on arrival, say, with different code per state. A table of rows is enough when the whole question is "what state comes next." A table is something you can print, diff, and hand to a non-engineer to review. A chain of subclasses is not.

sealed on TripState, or on EventKind, buys an exhaustiveness check wherever the transitions are matched with a switch elsewhere in the code. A state added later without a case for it then fails to compile instead of failing at runtime.

When naming it is wrong

Not every rule that looks like "what happens next" fits a two-column table. A no-show timeout needs to know the state, the event, and how long the trip has sat waiting. That is three inputs, not two, and Edge only carries two. No number of rows can express a rule with a third input. The fix is a guard, checked after the table lookup and before the transition is logged, not a wider row shape bolted onto every existing entry.

The threshold: write the rule as a signature first, and count its inputs. Two inputs, and it belongs in the table as a row. Three or more, and it belongs in code, asked separately. A table built for a machine that never actually branches by more than state and event is the harder mistake to catch. It still compiles and still passes review. The missing piece is a guard nobody added, and a curveball later exposes exactly that.

Where this lives in the app

Syllabus item B6 measures the table against three requirement changes on corpus/trip-state-machine. One of the three is the no-show timeout, which needs a third input, and PATCH.md says in its own words that this rule does not belong in the table at all.

All reference pages