Skip to content

Booking Engine Architecture

The Problem

Scheduling N sequential services across M available resources with conflict detection.

Example: a client books a haircut (60 min, any of 3 stylists) followed by a color treatment (90 min, only colorist-specialist). The engine must find all valid combinations for a given date, respecting each resource's free windows, buffers, and existing appointments.


Components

Component Role
ChainFinder Recursive backtracking search — finds all valid slot chains
BookingScorer Post-search ranking — scores and re-sorts solutions without touching the search
SlotCalculator Low-level arithmetic — splits windows, merges busy intervals, aligns to grid

Scoring is fully separated from search. ChainFinder returns raw solutions; BookingScorer evaluates them independently. Changing scoring weights never affects which slots are found.


Data Flow

```mermaid flowchart TD A["BookingEngineRequest\n+ list[MasterAvailability]"] B["ChainFinder.find()"] C["Backtracking loop\n_SlotCandidate (slots only)"] D["EngineResult\n(raw solutions, score=0)"] E["BookingScorer.score()"] F["EngineResult\n(scored + sorted descending)"]

A --> B --> C --> D --> E --> F

```

For multi-day searches (find_nearest), ChainFinder calls a user-supplied get_availability_for_date callable for each candidate date until solutions are found or search_days is exhausted.


BookingMode

Mode When to use
SINGLE_DAY All services on one calendar day. Default.
MULTI_DAY Services split across different days. (Planned — not yet implemented.)
MASTER_LOCKED Client is on a specific resource's personal page; only that resource is considered.

Provider Interfaces

The engine accepts plain MasterAvailability objects. Build them from your ORM using the provider interfaces:

class MyAvailabilityProvider:
    def build_masters_availability(
        self, master_ids: list[str], target_date: date, ...
    ) -> dict[str, MasterAvailability]:
        # query DB, merge schedules, subtract busy slots
        ...

AvailabilityProvider (single day) and build_availability_batch (date range for find_nearest) are the two integration points. The engine never imports your models.


Performance Design

During backtracking the engine uses lightweight _SlotCandidate objects decorated with __slots__. No Pydantic validation happens inside the recursion. SingleServiceSolution and BookingChainSolution (full Pydantic DTOs) are materialized only for the final result set, keeping the hot path allocation-minimal.


Quick Start

from codex_services.booking.slot_master import find_slots
from datetime import date

result = find_slots(
    request_data={
        "service_requests": [
            {"service_id": "haircut", "duration_minutes": 60, "possible_master_ids": ["m1", "m2"]},
            {"service_id": "color",   "duration_minutes": 90, "possible_master_ids": ["m2"]},
        ],
        "booking_date": str(date.today()),
    },
    resources_availability=[
        {"master_id": "m1", "free_windows": [["2024-05-15T09:00:00", "2024-05-15T18:00:00"]]},
        {"master_id": "m2", "free_windows": [["2024-05-15T09:00:00", "2024-05-15T18:00:00"]]},
    ],
)
if result["has_solutions"]:
    print(result["solutions"][0]["starts_at"])

See Also