| Version 5 (modified by , 9 days ago) ( diff ) |
|---|
P8 – Advanced Application Development (Transactions, Pooling)
Overview
This phase extends the foundational transaction concepts with concrete, schema-specific examples derived directly from the approved normalized database. Each transaction targets a real business scenario in the Wedding Planner application and references the exact tables from the schema.
In a production environment, these concepts ensure data consistency, correctness, and performance when multiple users interact with the system simultaneously.
Transaction 1 – Full Wedding Bundle Booking
Business Scenario
When a user finalises their wedding plan, all four service providers must be booked atomically: venue, photographer, band, and registrar. If any single booking fails (conflict, capacity, data error), none should be persisted.
Tables Involved
- venue_booking – insert + time-overlap check on venue_id + date
- photographer_booking – insert + overlap check on photographer_id + date
- band_booking – insert + overlap check on band_id + date
- registrar_booking – insert + overlap check on registrar_id + date
- wedding – update notes after all bookings succeed
Conflict Detection Logic
All four booking tables share the same overlap predicate. Two time windows overlap when:
start_time < $new_end_time AND end_time > $new_start_time
Full Transaction – SQL
BEGIN;
```
• STEP 1: Check venue availability
```
SELECT 1 FROM project.venue_booking
WHERE venue_id = 1
AND date = '2026-06-20'
AND start_time < '23:00'
AND end_time > '16:00'
AND status <> 'cancelled';
-- If any row returned → ROLLBACK (conflict)
```
• STEP 2: Check photographer availability
```
SELECT 1 FROM project.photographer_booking
WHERE photographer_id = 1
AND date = '2026-06-20'
AND start_time < '22:00'
AND end_time > '14:00'
AND status <> 'cancelled';
```
• STEP 3: Check band availability
```
SELECT 1 FROM project.band_booking
WHERE band_id = 1
AND date = '2026-06-20'
AND start_time < '23:00'
AND end_time > '18:00'
AND status <> 'cancelled';
```
• STEP 4: Check registrar availability
```
SELECT 1 FROM project.registrar_booking
WHERE registrar_id = 1
AND date = '2026-06-20'
AND start_time < '10:30'
AND end_time > '10:00'
AND status <> 'cancelled';
```
• STEP 5: Insert all four bookings
```
INSERT INTO project.venue_booking
(date, start_time, end_time, status, price, venue_id, wedding_id)
VALUES ('2026-06-20','16:00','23:00','confirmed',7000,1,1);
INSERT INTO project.photographer_booking
(date, start_time, end_time, status, photographer_id, wedding_id)
VALUES ('2026-06-20','14:00','22:00','confirmed',1,1);
INSERT INTO project.band_booking
(date, start_time, end_time, status, band_id, wedding_id)
VALUES ('2026-06-20','18:00','23:00','confirmed',1,1);
INSERT INTO project.registrar_booking
(date, start_time, end_time, status, registrar_id, wedding_id)
VALUES ('2026-06-20','10:00','10:30','confirmed',1,1);
-- STEP 6: Update wedding record
UPDATE project.wedding
SET notes = 'All services confirmed'
WHERE wedding_id = 1;
COMMIT;
-- On any error at any step: ROLLBACK;
Flask Implementation (SQLAlchemy & Python)
from sqlalchemy.exc import SQLAlchemyError
def book_full_wedding(wedding_id, venue_cfg, photo_cfg, band_cfg, reg_cfg):
try:
db.session.begin()
```
# Conflict checks – raise ValueError if any overlap found
_check_overlap('venue_booking', 'venue_id', venue_cfg)
_check_overlap('photographer_booking', 'photographer_id', photo_cfg)
_check_overlap('band_booking', 'band_id', band_cfg)
_check_overlap('registrar_booking', 'registrar_id', reg_cfg)
# Inserts
db.session.add(VenueBooking(**venue_cfg, wedding_id=wedding_id))
db.session.add(PhotographerBooking(**photo_cfg, wedding_id=wedding_id))
db.session.add(BandBooking(**band_cfg, wedding_id=wedding_id))
db.session.add(RegistrarBooking(**reg_cfg, wedding_id=wedding_id))
db.session.query(Wedding).filter_by(wedding_id=wedding_id)\
.update({'notes': 'All services confirmed'})
db.session.commit()
return {'status': 'ok', 'wedding_id': wedding_id}
except (ValueError, SQLAlchemyError) as e:
db.session.rollback()
return {'status': 'error', 'reason': str(e)}, 409
```
Transaction 2 – Guest Registration with Automatic RSVP Seeding
Business Scenario
When a new guest is added to a wedding, they must immediately receive a pending RSVP record for every existing event of that wedding (ceremony, registry, wedding etc.). This guarantees no event is orphaned without a guest decision, and no guest can appear in attendance without a prior RSVP.
Tables Involved
- guest – insert the new guest record
- event – read all events for this wedding_id
- event_rsvp – insert one pending RSVP per event (enforced by uq_rsvp: guest_id + event_id)
Why a Transaction is Required
Without a transaction, a crash would happen after inserting the guest but before inserting the RSVPs, leaving a guest with no RSVP rows. The UNIQUE constraint on event_rsvp (guest_id, event_id) prevents duplicates but cannot help if the insert never happened.
Full Transaction – SQL
BEGIN;
```
• STEP 1: Insert new guest
```
INSERT INTO project.guest (first_name, last_name, email, wedding_id)
VALUES ('Marija', 'Ilievska', '[marija@gmail.com](mailto:marija@gmail.com)', 1)
RETURNING guest_id;
```
• STEP 2: Insert a 'pending' RSVP for every event of this wedding
```
INSERT INTO project.event_rsvp (status, response_date, guest_id, event_id)
SELECT
'pending',
CURRENT_DATE,
:new_guest_id,
e.event_id
FROM project.event e
WHERE e.wedding_id = 1;
COMMIT;
-- Error: ROLLBACK;
Flask Implementation
def register_guest(wedding_id, first_name, last_name, email):
try:
db.session.begin()
```
guest = Guest(first_name=first_name, last_name=last_name,
email=email, wedding_id=wedding_id)
db.session.add(guest)
db.session.flush()
events = db.session.query(Event)\
.filter_by(wedding_id=wedding_id).all()
for event in events:
rsvp = EventRSVP(
status='pending',
response_date=date.today(),
guest_id=guest.guest_id,
event_id=event.event_id
)
db.session.add(rsvp)
db.session.commit()
return {'guest_id': guest.guest_id, 'rsvps_created': len(events)}
except SQLAlchemyError as e:
db.session.rollback()
return {'error': str(e)}, 500
```
Key pattern: db.session.flush() is used to obtain the new guest_id from the database sequence before the transaction commits, so it can be used in the RSVP inserts within the same transaction.
Transaction 3 – RSVP Acceptance and Seat Assignment
Business Scenario
When a guest accepts an RSVP, the system must simultaneously update event_rsvp to accepted and create or update the attendance record with a table number. These two operations are inseparable: an accepted RSVP with no attendance row causes inconsistency.
Tables Involved
- event_rsvp – update status from pending to accepted
- attendance – UPSERT the row for this guest_id + event_id using the uq_attendance constraint
Full Transaction
BEGIN;
```
• STEP 1: Update RSVP status
```
UPDATE project.event_rsvp
SET status = 'accepted',
response_date = CURRENT_DATE
WHERE guest_id = 1
AND event_id = 2;
else ROLLBACK
```
• STEP 2: Upsert attendance record
```
INSERT INTO project.attendance
(status, table_number, role, guest_id, event_id)
VALUES
('attending', 5, 'Guest', 1, 2)
ON CONFLICT (guest_id, event_id)
DO UPDATE SET
status = EXCLUDED.status,
table_number = EXCLUDED.table_number,
role = EXCLUDED.role;
COMMIT;
-- Error: ROLLBACK;
Key pattern
The attendance table has UNIQUE (guest_id, event_id). A guest may need to be reassigned to a different table later. Using ON CONFLICT DO UPDATE handles both the initial seat assignment and any re-assignment atomically, avoiding race conditions.
Transaction 4 – Wedding Cancellation with Status Cascade
Business Scenario
When a wedding is cancelled, every resource tied to it must be cancelled atomically: all service bookings (venue, photographer, band, registrar), all events, and all attendance records. A partial cancellation would leave inconsistent state.
Tables Involved
- wedding – update status/notes
- venue_booking – cancel bookings
- photographer_booking – cancel bookings
- band_booking – cancel bookings
- registrar_booking – cancel bookings
- event – cancel events
- attendance – mark attendance absent
Full Transaction – SQL
BEGIN; UPDATE project.wedding SET notes = 'Wedding cancelled' WHERE wedding_id = 1; UPDATE project.venue_booking SET status = 'cancelled' WHERE wedding_id = 1; UPDATE project.photographer_booking SET status = 'cancelled' WHERE wedding_id = 1; UPDATE project.band_booking SET status = 'cancelled' WHERE wedding_id = 1; UPDATE project.registrar_booking SET status = 'cancelled' WHERE wedding_id = 1; UPDATE project.event SET status = 'cancelled' WHERE wedding_id = 1; UPDATE project.attendance SET status = 'absent' WHERE event_id IN ( SELECT event_id FROM project.event WHERE wedding_id = 1 ); COMMIT; -- Error: ROLLBACK;
Concurrency Risk Without Transaction
A concurrent RSVP or attendance insert could occur between steps, causing inconsistent data tied to a cancelled wedding.
Transaction 5 – Budget Validation with Row Locking
Business Scenario
Before finalising a wedding, the system must ensure total cost does not exceed budget. The check and update must be atomic.
Tables Involved
- wedding – locked using FOR UPDATE
- venue_booking
- photographer_booking
- band_booking
- registrar_booking
Full Transaction – SQL
BEGIN; SELECT budget FROM project.wedding WHERE wedding_id = 1 FOR UPDATE; -- Calculate total cost across all services IF total_cost > budget THEN ROLLBACK; END IF; UPDATE project.wedding SET notes = 'Budget validated – wedding finalised' WHERE wedding_id = 1; COMMIT;
Why FOR UPDATE matters
Prevents TOCTOU race conditions by locking the wedding row during validation.
Savepoints – Partial Rollback
Concept
Savepoints allow partial rollback inside a transaction.
Example
BEGIN; INSERT venue_booking; INSERT photographer_booking; SAVEPOINT after_core; INSERT band_booking; ROLLBACK TO SAVEPOINT after_core; COMMIT;
Isolation Levels
Overview
Isolation defines how concurrent transactions behave.
Problems prevented:
- Dirty reads
- Non-repeatable reads
- Phantom reads
- Lost updates
Levels
- READ COMMITTED – basic reads
- REPEATABLE READ – RSVP consistency
- SERIALIZABLE – full booking + budget validation
SQLAlchemy
db.session.execute(text('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'))
Connection Pooling
Why needed
Pooling reuses DB connections instead of creating new ones per request.
Affected endpoints
- availability endpoints
- RSVP endpoints
- booking endpoints
Configuration
engine = create_engine( 'postgresql://user:pass@localhost/weddingdb', pool_size=10, max_overflow=20, pool_timeout=30, pool_recycle=1800, pool_pre_ping=True )
Parameters
- pool_size – base connections
- max_overflow – burst capacity
- pool_timeout – wait limit
- pool_recycle – prevents stale connections
- pool_pre_ping – checks connection health
Conclusion
This phase introduced five full, schema-specific transactions for the Wedding Planner system:
- Full wedding booking with atomic service allocation
- Guest registration with RSVP seeding
- RSVP acceptance with attendance UPSERT
- Wedding cancellation with full cascade
- Budget validation with row locking
These ensure ACID compliance, prevent race conditions, and guarantee consistent multi-table operations in a concurrent environment.
