Skip to content

Yoyaku Booking Engine

A lightweight, concurrency-safe resource booking and availability API for Drupal 11.

Yoyaku is the missing capacity-safe layer for reservations. It gives any module a small set of entities and a service API to:

  • define resources (rooms, equipment, events, …);
  • publish availability slots, each split into one or more tariffs (the "sub-slots": Adult / Child / VIP, Standard / Premium, …) with their own capacity;
  • take bookings that reserve capacity without ever overbooking - even under concurrent requests - using a hold → confirm lifecycle with automatic expiry.

Why it exists

Most Drupal booking solutions count rows to decide availability, which races under load and double-books the last unit. Yoyaku treats per-tariff capacity as a first-class, concurrency-safe concern: every hold locks the anchor row of each scope it consumes and checks capacity under those locks, inside one database transaction.

flowchart LR
    subgraph Consumers
        UI[yoyaku_calendar / yoyaku_webform]
        WF[Orchestra workflow]
        PAY[Payment]
    end
    Consumers -->|hold / confirm / release| API[BookingManager API]
    API -->|anchor rows FOR UPDATE| DB[(Capacity per scope)]
    API -->|dispatches| EV[Lifecycle events]
    EV --> Consumers

What it is not

  • Not a UI builder - the core is headless. Calendars and forms live in submodules.
  • Not a payment system - price, guarantees, currency and tax are consumer concerns. The engine stays money-agnostic; see Paying for a booking.
  • Not a scheduler - recurrence is an optional layer that materializes concrete slots; see Recurrence.

Quick mental model

Concept One-liner
Resource A bookable thing, with defaults and a booking window.
Slot A concrete time window of a resource.
Tariff Something a resource offers, such as "adult"; a slot tariff is how one slot departs from it.
Booking Consumes a tariff's quota. held → confirmed → completed or unused, or freed.
Tenant The realm a row belongs to (default: default).

Requirements

  • Drupal ^11.3
  • PHP >=8.3
  • drupal:options (core)
  • MySQL or MariaDB, not SQLite

The database is not a matter of taste here. Everything this module promises rests on two people never getting the same seat, and what prevents that is a row lock: a hold takes the anchor row of every scope it consumes with SELECT … FOR UPDATE, so a second request waits for the first to commit instead of reading a stale answer beside it. Drupal's SQLite driver drops the clause silently, which leaves the locking read an ordinary read and the guarantee unenforced. PostgreSQL implements it, and the test suite passes on it, though continuous integration runs on MySQL. The mechanism is in Capacity and holds, and it is measured under Performance results, and what it means for a box on a rush day is in Sizing for a rush.

Next steps