Event Booking¶
A transactional event booking system that atomically reserves seats and creates booking records, with automatic rollback when an event is sold out.
What You'll Learn¶
- How to enable Forage-managed transactions with
forage.jdbc.transaction.enabled=true - Using Camel's
transactedDSL for ACID database operations - Optimistic concurrency control with conditional SQL updates
- File-based event-driven processing with error handling
Prerequisites¶
Start PostgreSQL:
Then create the events and bookings tables with sample data:
What setup-db.sh does
CREATE TABLE IF NOT EXISTS events (
event_id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
available_seats INT NOT NULL CHECK (available_seats >= 0)
);
CREATE TABLE IF NOT EXISTS bookings (
booking_id SERIAL PRIMARY KEY,
event_id INT NOT NULL REFERENCES events(event_id),
user_id INT NOT NULL,
booking_time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_bookings_event_id ON bookings(event_id);
-- Sample events
INSERT INTO events (name, available_seats) VALUES ('Camel Development Conference', 150);
INSERT INTO events (name, available_seats) VALUES ('Advanced Messaging Workshop', 1);
The second event has only 1 seat -- this is used to demonstrate the sold-out rollback scenario.
Configuration¶
application.properties
# Database connection
forage.jdbc.url=jdbc:postgresql://localhost:5432/postgres # (1)!
forage.jdbc.username=test
forage.jdbc.password=test
forage.jdbc.db.kind=postgresql
# Connection pool
forage.jdbc.pool.initial.size=5
forage.jdbc.pool.min.size=2
forage.jdbc.pool.max.size=20
forage.jdbc.pool.acquisition.timeout.seconds=5
forage.jdbc.pool.validation.timeout.seconds=3
forage.jdbc.pool.leak.timeout.minutes=10
forage.jdbc.pool.idle.validation.timeout.minutes=3
# Transaction
forage.jdbc.transaction.enabled=true # (2)!
forage.jdbc.transaction.timeout.seconds=30
- Uses the default (unnamed) prefix, so the datasource bean is registered as
dataSource. - Enables Forage-managed transactions. This registers a
PlatformTransactionManagerthat Camel'stransactedDSL can discover automatically.
Route¶
book.camel.yaml
# Route 1: File watcher -- picks up JSON booking files
- route:
id: file-to-booking-route
from:
uri: file
parameters:
delete: true
directoryName: data/inbox
moveFailed: .error # (1)!
steps:
- log:
message: "New booking file detected: ${header.CamelFileName}"
- to:
uri: direct
parameters:
name: bookEvent
- log:
message: "Successfully processed booking file: ${header.CamelFileNameOnly}"
# Route 2: Transactional booking logic
- route:
id: event-booking-route
from:
uri: direct
parameters:
name: bookEvent
steps:
- transacted: {} # (2)!
- log:
message: >-
Attempting to book seat for event ${jq(.eventId)}
for user ${jq(.userId)}
- toD: # (3)!
uri: sql
parameters:
dataSource: "#dataSource"
query: >-
UPDATE events SET available_seats = available_seats - 1
WHERE event_id = ${jq(.eventId)} AND available_seats > 0
- choice:
when:
- simple:
expression: ${header.CamelSqlUpdateCount} == 1 # (4)!
steps:
- log:
message: "Seat successfully reserved for event ${jq(.eventId)}."
- toD:
uri: sql
parameters:
dataSource: "#dataSource"
query: >-
INSERT INTO bookings (event_id, user_id)
VALUES (${jq(.eventId)}, ${jq(.userId)})
- log:
message: "Booking created for user ${jq(.userId)}. Transaction complete."
otherwise:
steps:
- log:
message: "Event ${jq(.eventId)} is sold out! Rolling back transaction."
- throwException: # (5)!
exceptionType: java.lang.RuntimeException
message: Event is sold out
- Failed bookings move the file to
data/inbox/.errorinstead of deleting it. - Starts a JTA transaction -- all subsequent SQL statements participate in the same transaction.
toD(dynamic to) is used because the query containsjqexpressions resolved at runtime.- The
WHERE available_seats > 0guard means the UPDATE affects 0 rows when the event is sold out. - Throwing an exception inside a
transactedblock triggers an automatic rollback.
Transaction flow¶
- The
transactedstep begins a database transaction. - An
UPDATEdecrementsavailable_seatsonly if seats remain (available_seats > 0). - If exactly one row was updated, a booking record is inserted and the transaction commits.
- If zero rows were updated (sold out), an exception is thrown and the entire transaction rolls back -- the seat count stays unchanged.
Running¶
Testing the scenarios¶
Copy the sample booking files into the inbox to exercise the three scenarios:
# Successful booking -- "Camel Development Conference" has 150 seats
cp booking-1.json data/inbox/
# Last seat -- "Advanced Messaging Workshop" has 1 seat
cp booking-2.json data/inbox/
# Sold out -- same event, 0 seats remaining after booking-2
cp booking-3.json data/inbox/
| File | Event | Outcome |
|---|---|---|
booking-1.json | Camel Development Conference (150 seats) | Booking created, seats: 150 -> 149 |
booking-2.json | Advanced Messaging Workshop (1 seat) | Booking created, seats: 1 -> 0 |
booking-3.json | Advanced Messaging Workshop (0 seats) | Transaction rolled back, file moved to .error |
Key Takeaways¶
- Setting
forage.jdbc.transaction.enabled=trueregisters a transaction manager that Camel'stransactedDSL discovers automatically. - Optimistic concurrency (
WHERE available_seats > 0) prevents overselling without pessimistic locks. - Throwing an exception inside a
transactedblock rolls back all SQL statements in that transaction. - The file component's
moveFailedparameter provides a dead-letter directory for failed bookings.