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¶
- Follow the no-code Getting started guide to publish a bookable resource and put a calendar on your site.
- Understand the concepts and architecture.
- Learn the capacity and hold model - the heart of the module.
- Read the API reference to start booking from your own code.