Skip to content

The Orchestrator Pattern

TL;DR: Every LLD design has one top-level coordinator that owns domain objects, exposes the public API, and routes inputs to the right places. Start your design here. This is technically the Facade pattern, but you don't need to name it.

Orchestrator Pattern

The One Class You Always Start With

Look at any LLD problem and you'll find a natural coordinator:

Problem Orchestrator
Parking Lot ParkingLot
Tic Tac Toe Game
Movie Booking BookingSystem
File System FileSystem
Rate Limiter RateLimiter
Inventory System InventoryManager
Amazon Locker LockerSystem
Library Management Library

This class is the entry point. When the interviewer says "walk me through the flow," you start here. When they say "how does a user park their car," the answer begins with parkingLot.enter(vehicleType, licensePlate).

What the Orchestrator Always Does

Every orchestrator has the same three responsibilities:

1. Owns the Collection of Domain Objects

The orchestrator holds the core data structures that represent the system's state.

class ParkingLot {
    private final List<SpotSize> spots;              // all spots and their sizes
    private final Set<Integer> occupiedSpotIds;       // which spots are taken
    private final Map<String, Ticket> activeTickets;  // license plate -> ticket
}
class ParkingLot {
    std::vector<SpotSize> spots;
    std::unordered_set<int> occupiedSpotIds;
    std::unordered_map<std::string, Ticket> activeTickets;
};
class ParkingLot:
    def __init__(self, spot_sizes: list[SpotSize]):
        self._spots: list[SpotSize] = spot_sizes
        self._occupied_spot_ids: set[int] = set()
        self._active_tickets: dict[str, Ticket] = {}

2. Provides the Public API

The orchestrator's methods ARE the system's interface. Every operation the system supports is a method on this class.

class ParkingLot {
    public Ticket enter(VehicleType vehicleType, String licensePlate) { ... }
    public Receipt exit(String licensePlate) { ... }
    public int getAvailableSpots(SpotSize size) { ... }
}
class ParkingLot {
public:
    Ticket enter(VehicleType vehicleType, const std::string& licensePlate);
    Receipt exit(const std::string& licensePlate);
    int getAvailableSpots(SpotSize size) const;
};
class ParkingLot:
    def enter(self, vehicle_type: VehicleType, license_plate: str) -> Ticket: ...
    def exit(self, license_plate: str) -> Receipt: ...
    def get_available_spots(self, size: SpotSize) -> int: ...

3. Routes Inputs and Handles Cross-Cutting Operations

The orchestrator coordinates between domain objects. It doesn't do everything itself — it delegates to specialized classes — but it decides who does what.

Design Top-Down: Start From the Orchestrator

The most effective way to design in an interview is top-down. Start with the orchestrator's public methods, then figure out what helper classes you need to implement them.

Here's the full Parking Lot orchestrator:

class ParkingLot {
    private final SpotSize[] spots;
    private final Set<Integer> occupiedSpotIds;
    private final Map<String, Ticket> activeTickets;
    private final FeeCalculator feeCalculator;

    public ParkingLot(SpotSize[] spotSizes, FeeCalculator feeCalculator) {
        this.spots = spotSizes;
        this.occupiedSpotIds = new HashSet<>();
        this.activeTickets = new HashMap<>();
        this.feeCalculator = feeCalculator;
    }

    public Ticket enter(VehicleType vehicleType, String licensePlate) {
        if (activeTickets.containsKey(licensePlate)) {
            throw new IllegalStateException("Vehicle already parked");
        }
        SpotSize requiredSize = mapVehicleToSpot(vehicleType);
        int spotId = findAvailableSpot(requiredSize);
        if (spotId == -1) {
            throw new IllegalStateException("No available spots for this size");
        }
        occupiedSpotIds.add(spotId);
        Ticket ticket = new Ticket(spotId, licensePlate, Instant.now());
        activeTickets.put(licensePlate, ticket);
        return ticket;
    }

    public Receipt exit(String licensePlate) {
        Ticket ticket = activeTickets.remove(licensePlate);
        if (ticket == null) {
            throw new IllegalArgumentException("No active ticket for this plate");
        }
        occupiedSpotIds.remove(ticket.getSpotId());
        double fee = feeCalculator.calculate(ticket.getEntryTime(), Instant.now());
        return new Receipt(ticket, fee);
    }

    private int findAvailableSpot(SpotSize size) {
        for (int i = 0; i < spots.length; i++) {
            if (spots[i] == size && !occupiedSpotIds.contains(i)) {
                return i;
            }
        }
        return -1;
    }

    private SpotSize mapVehicleToSpot(VehicleType type) {
        return switch (type) {
            case MOTORCYCLE -> SpotSize.SMALL;
            case CAR -> SpotSize.MEDIUM;
            case TRUCK -> SpotSize.LARGE;
        };
    }
}
class ParkingLot {
    std::vector<SpotSize> spots;
    std::unordered_set<int> occupiedSpotIds;
    std::unordered_map<std::string, Ticket> activeTickets;
    FeeCalculator feeCalculator;

public:
    ParkingLot(std::vector<SpotSize> spotSizes, FeeCalculator calculator)
        : spots(std::move(spotSizes)), feeCalculator(std::move(calculator)) {}

    Ticket enter(VehicleType vehicleType, const std::string& licensePlate) {
        if (activeTickets.count(licensePlate)) {
            throw std::runtime_error("Vehicle already parked");
        }
        SpotSize requiredSize = mapVehicleToSpot(vehicleType);
        int spotId = findAvailableSpot(requiredSize);
        if (spotId == -1) {
            throw std::runtime_error("No available spots for this size");
        }
        occupiedSpotIds.insert(spotId);
        Ticket ticket(spotId, licensePlate, Clock::now());
        activeTickets[licensePlate] = ticket;
        return ticket;
    }

    Receipt exit(const std::string& licensePlate) {
        auto it = activeTickets.find(licensePlate);
        if (it == activeTickets.end()) {
            throw std::invalid_argument("No active ticket for this plate");
        }
        Ticket ticket = it->second;
        activeTickets.erase(it);
        occupiedSpotIds.erase(ticket.getSpotId());
        double fee = feeCalculator.calculate(ticket.getEntryTime(), Clock::now());
        return Receipt(ticket, fee);
    }

private:
    int findAvailableSpot(SpotSize size) const {
        for (int i = 0; i < static_cast<int>(spots.size()); i++) {
            if (spots[i] == size && occupiedSpotIds.find(i) == occupiedSpotIds.end()) {
                return i;
            }
        }
        return -1;
    }

    SpotSize mapVehicleToSpot(VehicleType type) const {
        switch (type) {
            case VehicleType::MOTORCYCLE: return SpotSize::SMALL;
            case VehicleType::CAR:        return SpotSize::MEDIUM;
            case VehicleType::TRUCK:      return SpotSize::LARGE;
        }
    }
};
class ParkingLot:
    def __init__(self, spot_sizes: list[SpotSize], fee_calculator: FeeCalculator):
        self._spots = spot_sizes
        self._occupied_spot_ids: set[int] = set()
        self._active_tickets: dict[str, Ticket] = {}
        self._fee_calculator = fee_calculator

    def enter(self, vehicle_type: VehicleType, license_plate: str) -> Ticket:
        if license_plate in self._active_tickets:
            raise ValueError("Vehicle already parked")
        required_size = self._map_vehicle_to_spot(vehicle_type)
        spot_id = self._find_available_spot(required_size)
        if spot_id == -1:
            raise ValueError("No available spots for this size")
        self._occupied_spot_ids.add(spot_id)
        ticket = Ticket(spot_id, license_plate, datetime.now())
        self._active_tickets[license_plate] = ticket
        return ticket

    def exit(self, license_plate: str) -> Receipt:
        ticket = self._active_tickets.pop(license_plate, None)
        if ticket is None:
            raise ValueError("No active ticket for this plate")
        self._occupied_spot_ids.discard(ticket.spot_id)
        fee = self._fee_calculator.calculate(ticket.entry_time, datetime.now())
        return Receipt(ticket, fee)

    def _find_available_spot(self, size: SpotSize) -> int:
        for i, spot_size in enumerate(self._spots):
            if spot_size == size and i not in self._occupied_spot_ids:
                return i
        return -1

    def _map_vehicle_to_spot(self, vehicle_type: VehicleType) -> SpotSize:
        mapping = {
            VehicleType.MOTORCYCLE: SpotSize.SMALL,
            VehicleType.CAR: SpotSize.MEDIUM,
            VehicleType.TRUCK: SpotSize.LARGE,
        }
        return mapping[vehicle_type]

Notice how designing top-down revealed the need for Ticket, Receipt, FeeCalculator, and the enums — they emerged naturally from implementing the orchestrator's methods.

Interview tip: Start writing the orchestrator's enter() or play() or book() method. As you hit something that needs its own logic — fee calculation, win detection, seat assignment — say "I'll extract this into a FeeCalculator class" and move on. This top-down discovery is exactly what interviewers want to see.

More Orchestrator Examples

BookingSystem: Movie Ticket Booking

The BookingSystem coordinates showtimes, seat availability, and reservations. Without it, seat booking logic would be scattered across Showtime, Booking, and potentially a Screen class.

class BookingSystem {
    private final Map<String, Showtime> showtimes;  // showtimeId -> Showtime

    public BookingSystem() {
        this.showtimes = new HashMap<>();
    }

    public void addShowtime(String showtimeId, String movieTitle, 
                            int rows, int cols) {
        showtimes.put(showtimeId, new Showtime(showtimeId, movieTitle, rows, cols));
    }

    public Booking bookSeats(String showtimeId, String userId, 
                             List<String> seatIds) {
        Showtime showtime = getShowtime(showtimeId);
        for (String seatId : seatIds) {
            if (!showtime.isSeatAvailable(seatId)) {
                throw new IllegalStateException("Seat " + seatId + " is not available");
            }
        }
        // Reserve all seats atomically
        for (String seatId : seatIds) {
            showtime.reserveSeat(seatId);
        }
        return new Booking(UUID.randomUUID().toString(), showtimeId, 
                          userId, seatIds, Instant.now());
    }

    public void cancelBooking(Booking booking) {
        Showtime showtime = getShowtime(booking.getShowtimeId());
        for (String seatId : booking.getSeatIds()) {
            showtime.releaseSeat(seatId);
        }
    }

    public List<String> getAvailableSeats(String showtimeId) {
        return getShowtime(showtimeId).getAvailableSeats();
    }

    private Showtime getShowtime(String id) {
        Showtime s = showtimes.get(id);
        if (s == null) throw new IllegalArgumentException("Showtime not found: " + id);
        return s;
    }
}
class BookingSystem {
    std::unordered_map<std::string, Showtime> showtimes;

public:
    void addShowtime(const std::string& id, const std::string& movieTitle,
                     int rows, int cols) {
        showtimes.emplace(id, Showtime(id, movieTitle, rows, cols));
    }

    Booking bookSeats(const std::string& showtimeId, const std::string& userId,
                      const std::vector<std::string>& seatIds) {
        auto& showtime = getShowtime(showtimeId);
        for (const auto& seatId : seatIds) {
            if (!showtime.isSeatAvailable(seatId))
                throw std::runtime_error("Seat " + seatId + " is not available");
        }
        for (const auto& seatId : seatIds) {
            showtime.reserveSeat(seatId);
        }
        return Booking(generateId(), showtimeId, userId, seatIds, Clock::now());
    }

    void cancelBooking(const Booking& booking) {
        auto& showtime = getShowtime(booking.getShowtimeId());
        for (const auto& seatId : booking.getSeatIds()) {
            showtime.releaseSeat(seatId);
        }
    }

private:
    Showtime& getShowtime(const std::string& id) {
        auto it = showtimes.find(id);
        if (it == showtimes.end()) throw std::invalid_argument("Showtime not found");
        return it->second;
    }
};
class BookingSystem:
    def __init__(self):
        self._showtimes: dict[str, Showtime] = {}

    def add_showtime(self, showtime_id: str, movie_title: str, 
                     rows: int, cols: int) -> None:
        self._showtimes[showtime_id] = Showtime(showtime_id, movie_title, rows, cols)

    def book_seats(self, showtime_id: str, user_id: str, 
                   seat_ids: list[str]) -> Booking:
        showtime = self._get_showtime(showtime_id)
        for seat_id in seat_ids:
            if not showtime.is_seat_available(seat_id):
                raise ValueError(f"Seat {seat_id} is not available")
        for seat_id in seat_ids:
            showtime.reserve_seat(seat_id)
        return Booking(str(uuid4()), showtime_id, user_id, seat_ids, datetime.now())

    def cancel_booking(self, booking: Booking) -> None:
        showtime = self._get_showtime(booking.showtime_id)
        for seat_id in booking.seat_ids:
            showtime.release_seat(seat_id)

    def _get_showtime(self, showtime_id: str) -> Showtime:
        showtime = self._showtimes.get(showtime_id)
        if showtime is None:
            raise ValueError(f"Showtime not found: {showtime_id}")
        return showtime

Notice the orchestrator pattern: BookingSystem doesn't know how seats are stored (that's Showtime's job). It doesn't know how bookings are structured (that's Booking's job). It coordinates: "check availability, then reserve, then create a record."

InventoryManager: Warehouse Stock Tracking

class InventoryManager {
    private final Map<String, Map<String, Integer>> stock;  // warehouseId -> (sku -> qty)
    private final List<StockMovement> movementLog;

    public InventoryManager() {
        this.stock = new HashMap<>();
        this.movementLog = new ArrayList<>();
    }

    public void addWarehouse(String warehouseId) {
        stock.putIfAbsent(warehouseId, new HashMap<>());
    }

    public void receive(String warehouseId, String sku, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
        Map<String, Integer> warehouseStock = getWarehouse(warehouseId);
        warehouseStock.merge(sku, quantity, Integer::sum);
        movementLog.add(new StockMovement(warehouseId, sku, quantity, 
                                          MovementType.RECEIVE, Instant.now()));
    }

    public void ship(String warehouseId, String sku, int quantity) {
        if (quantity <= 0) throw new IllegalArgumentException("Quantity must be positive");
        Map<String, Integer> warehouseStock = getWarehouse(warehouseId);
        int current = warehouseStock.getOrDefault(sku, 0);
        if (current < quantity) {
            throw new IllegalStateException(
                "Insufficient stock: have " + current + ", need " + quantity);
        }
        warehouseStock.put(sku, current - quantity);
        movementLog.add(new StockMovement(warehouseId, sku, -quantity, 
                                          MovementType.SHIP, Instant.now()));
    }

    public void transfer(String fromWarehouse, String toWarehouse, 
                         String sku, int quantity) {
        ship(fromWarehouse, sku, quantity);
        receive(toWarehouse, sku, quantity);
    }

    public int getStock(String warehouseId, String sku) {
        return getWarehouse(warehouseId).getOrDefault(sku, 0);
    }

    private Map<String, Integer> getWarehouse(String id) {
        Map<String, Integer> w = stock.get(id);
        if (w == null) throw new IllegalArgumentException("Warehouse not found: " + id);
        return w;
    }
}
class InventoryManager:
    def __init__(self):
        self._stock: dict[str, dict[str, int]] = {}  # warehouse -> {sku: qty}
        self._movement_log: list[StockMovement] = []

    def add_warehouse(self, warehouse_id: str) -> None:
        self._stock.setdefault(warehouse_id, {})

    def receive(self, warehouse_id: str, sku: str, quantity: int) -> None:
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        warehouse = self._get_warehouse(warehouse_id)
        warehouse[sku] = warehouse.get(sku, 0) + quantity
        self._movement_log.append(
            StockMovement(warehouse_id, sku, quantity, MovementType.RECEIVE))

    def ship(self, warehouse_id: str, sku: str, quantity: int) -> None:
        if quantity <= 0:
            raise ValueError("Quantity must be positive")
        warehouse = self._get_warehouse(warehouse_id)
        current = warehouse.get(sku, 0)
        if current < quantity:
            raise ValueError(f"Insufficient stock: have {current}, need {quantity}")
        warehouse[sku] = current - quantity
        self._movement_log.append(
            StockMovement(warehouse_id, sku, -quantity, MovementType.SHIP))

    def transfer(self, from_wh: str, to_wh: str, sku: str, quantity: int) -> None:
        self.ship(from_wh, sku, quantity)
        self.receive(to_wh, sku, quantity)

The transfer() method is a great example of orchestration -- it composes two existing operations (ship + receive) without duplicating their logic.

ChessGame: Complex State Management

A chess game orchestrator is more complex because it manages turn alternation, move validation, check/checkmate detection, and game state transitions.

class ChessGame {
    private final Board board;
    private Color currentTurn;
    private GameStatus status;
    private final List<Move> moveHistory;
    private final MoveValidator validator;

    public ChessGame() {
        this.board = new Board();  // sets up standard piece positions
        this.currentTurn = Color.WHITE;
        this.status = GameStatus.ACTIVE;
        this.moveHistory = new ArrayList<>();
        this.validator = new MoveValidator();
    }

    public void makeMove(Position from, Position to) {
        if (status != GameStatus.ACTIVE) {
            throw new IllegalStateException("Game is over: " + status);
        }
        Piece piece = board.getPiece(from);
        if (piece == null) {
            throw new IllegalArgumentException("No piece at " + from);
        }
        if (piece.getColor() != currentTurn) {
            throw new IllegalArgumentException("Not your turn");
        }
        if (!validator.isLegal(board, from, to, piece)) {
            throw new IllegalArgumentException("Illegal move");
        }

        Move move = board.executeMove(from, to);
        moveHistory.add(move);

        // Check for game-ending conditions
        Color opponent = (currentTurn == Color.WHITE) ? Color.BLACK : Color.WHITE;
        if (validator.isCheckmate(board, opponent)) {
            status = GameStatus.CHECKMATE;
        } else if (validator.isStalemate(board, opponent)) {
            status = GameStatus.STALEMATE;
        } else {
            currentTurn = opponent;
        }
    }

    public void resign() {
        status = (currentTurn == Color.WHITE) 
            ? GameStatus.BLACK_WINS : GameStatus.WHITE_WINS;
    }

    public List<Position> getLegalMoves(Position from) {
        return validator.getLegalMoves(board, from);
    }
}
class ChessGame:
    def __init__(self):
        self._board = Board()
        self._current_turn = Color.WHITE
        self._status = GameStatus.ACTIVE
        self._move_history: list[Move] = []
        self._validator = MoveValidator()

    def make_move(self, from_pos: Position, to_pos: Position) -> None:
        if self._status != GameStatus.ACTIVE:
            raise RuntimeError(f"Game is over: {self._status}")
        piece = self._board.get_piece(from_pos)
        if piece is None:
            raise ValueError(f"No piece at {from_pos}")
        if piece.color != self._current_turn:
            raise ValueError("Not your turn")
        if not self._validator.is_legal(self._board, from_pos, to_pos, piece):
            raise ValueError("Illegal move")

        move = self._board.execute_move(from_pos, to_pos)
        self._move_history.append(move)

        opponent = Color.BLACK if self._current_turn == Color.WHITE else Color.WHITE
        if self._validator.is_checkmate(self._board, opponent):
            self._status = GameStatus.CHECKMATE
        elif self._validator.is_stalemate(self._board, opponent):
            self._status = GameStatus.STALEMATE
        else:
            self._current_turn = opponent

The ChessGame orchestrator coordinates Board (piece positions), MoveValidator (rule enforcement), and Move (history tracking). It doesn't know how pieces move -- that's the validator's job. It doesn't know how the board stores pieces -- that's the board's job. It orchestrates the flow: validate, execute, check end conditions, switch turn.

What Goes Wrong Without an Orchestrator

When there's no clear coordinator, business logic scatters across entities. Here's what a Parking Lot looks like without an orchestrator:

// BAD: Logic scattered across entities

class Vehicle {
    private VehicleType type;
    private String plate;

    // Vehicle knows how to find a spot?
    public ParkingSpot findSpot(List<ParkingSpot> spots) {
        for (ParkingSpot spot : spots) {
            if (spot.getSize() == this.type.getRequiredSize() && !spot.isOccupied()) {
                return spot;
            }
        }
        return null;
    }
}

class ParkingSpot {
    private boolean occupied;
    private Ticket ticket;

    // Spot creates its own ticket?
    public Ticket park(Vehicle vehicle) {
        this.occupied = true;
        this.ticket = new Ticket(this.id, vehicle.getPlate(), Instant.now());
        return this.ticket;
    }

    // Spot calculates fees?
    public double calculateFee() {
        Duration d = Duration.between(ticket.getEntryTime(), Instant.now());
        return d.toHours() * 2.50;
    }
}

class Ticket {
    // Ticket manages unparking?
    public void exitLot(ParkingSpot spot) {
        spot.setOccupied(false);
    }
}

Problems with this scattered design:

  1. No clear entry point. How does a car park? You need to call vehicle.findSpot(spots), then spot.park(vehicle). But who has the list of spots? Who orchestrates the sequence?
  2. Wrong responsibilities. A Vehicle shouldn't know about ParkingSpots. A Spot shouldn't create Tickets. A Ticket shouldn't manage Spots.
  3. Impossible to test in isolation. Testing fee calculation requires creating a Vehicle, finding a Spot, parking, and then calculating. With an orchestrator, you test feeCalculator.calculate(entryTime, exitTime) directly.
  4. Adding features breaks everything. Want to add "find the nearest available spot"? You'd modify Vehicle. Want to add surge pricing? You'd modify Spot. Want to add validation that a vehicle isn't already parked? Where does that even go?

Compare with the orchestrator version where ParkingLot.enter() handles the entire flow in one place, delegating specifics to focused helper classes.

When Does an Orchestrator Become a God Class?

The line between a healthy orchestrator and a god class is clear:

Orchestrator (healthy) God Class (unhealthy)
Receives requests and routes them Receives requests and does all the work
Delegates computation to helpers Contains all business logic directly
5-10 public methods 20+ public methods
Methods are 5-15 lines each Methods are 50+ lines each
"Calculate the fee" -> feeCalculator.calculate(...) "Calculate the fee" -> 30 lines of rate logic inline
Adding a feature means adding a new helper class Adding a feature means adding more code to the orchestrator

Concrete example of the line being crossed:

// HEALTHY: orchestrator delegates
class ParkingLot {
    public Receipt exit(String plate) {
        Ticket ticket = activeTickets.remove(plate);
        occupiedSpotIds.remove(ticket.getSpotId());
        double fee = feeCalculator.calculate(ticket.getEntryTime(), Instant.now());
        return new Receipt(ticket, fee);
    }
}

// GOD CLASS: orchestrator computes everything
class ParkingLot {
    public Receipt exit(String plate) {
        Ticket ticket = activeTickets.remove(plate);
        occupiedSpotIds.remove(ticket.getSpotId());

        // Fee calculation inlined -- this should be extracted
        Duration duration = Duration.between(ticket.getEntryTime(), Instant.now());
        long hours = duration.toHours();
        if (duration.toMinutes() % 60 > 0) hours++;  // round up
        double rate;
        if (ticket.getVehicleType() == VehicleType.MOTORCYCLE) rate = 1.0;
        else if (ticket.getVehicleType() == VehicleType.CAR) rate = 2.5;
        else rate = 5.0;
        if (hours > 12) rate *= 0.8;  // daily discount
        double fee = hours * rate;

        // Receipt formatting inlined -- this should be extracted
        String formatted = String.format("Plate: %s\nSpot: %d\nHours: %d\nFee: $%.2f",
            plate, ticket.getSpotId(), hours, fee);
        System.out.println(formatted);

        return new Receipt(ticket, fee);
    }
}

The second version's exit() method is doing three jobs: ticket management, fee calculation, and receipt formatting. Extract the fee logic into FeeCalculator and the formatting into ReceiptFormatter, and the orchestrator goes back to being a clean coordinator.

Rule of thumb: If a method in your orchestrator is longer than 15 lines, you probably need to extract a helper class.

Orchestrator vs Entity Responsibilities

Responsibility Belongs on Orchestrator Belongs on Entity
Managing a collection of objects Yes (list of tickets, set of spots) No
Validating business rules across objects Yes ("is this vehicle already parked?") No
Validating data within an object No Yes ("is this amount positive?")
Computing derived values No -- delegate to a calculator class Possibly, if simple (like isExpired())
Tracking lifecycle state of ONE object No Yes (Ticket created/used, Token valid/revoked)
Coordinating a multi-step operation Yes (find spot, create ticket, record entry) No
Exposing the system's public API Yes No -- entities are internal

Interview tip: When the interviewer asks "Why is enter() on ParkingLot and not on Vehicle or Spot?" this table gives you the answer: "Entering involves multiple objects -- finding a spot, creating a ticket, recording occupancy. That coordination is the orchestrator's job. Vehicle and Spot are data that gets coordinated, not coordinators themselves."

The Orchestrator Is Not a God Class

A common concern: "Isn't putting everything in one coordinator class a violation of SRP?"

No, because the orchestrator doesn't do everything — it coordinates everything. It delegates actual logic to specialized classes:

  • Fee calculation goes to FeeCalculator
  • Win detection goes to WinChecker
  • Seat assignment goes to SeatAssigner

The orchestrator's single responsibility is coordination: receiving requests, routing them to the right domain objects, and returning results. That's one reason to change — the system's workflow changes.

If your orchestrator starts containing business logic (parsing fee rules, checking diagonal wins, validating payment details), that's when it becomes a god class. The fix is extraction, not elimination.

Quick Recap

Concept What it means Why it matters
One orchestrator per design Every LLD problem has a natural top-level coordinator Gives you a clear starting point and API surface
Three responsibilities Owns domain objects, provides public API, routes inputs Defines what the orchestrator does (and what it delegates)
Design top-down Start from the orchestrator's methods, discover helper classes as you go Produces a coherent design that you can explain linearly
Coordinate, don't compute The orchestrator delegates business logic to specialized classes Keeps it from becoming a god class