wiki:P8

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.

Flask Implementation

from sqlalchemy.exc import SQLAlchemyError
from datetime import date

def accept_rsvp_and_assign_seat(guest_id, event_id, table_number, role='Guest'):
try:
db.session.begin()

    # STEP 1: Find RSVP record
    rsvp = db.session.query(EventRSVP).filter_by(
        guest_id=guest_id,
        event_id=event_id
    ).first()

    # RSVP must exist
    if not rsvp:
        raise ValueError('RSVP record not found')

    # STEP 2: Update RSVP status
    rsvp.status = 'accepted'
    rsvp.response_date = date.today()

    # STEP 3: Check if attendance already exists
    attendance = db.session.query(Attendance).filter_by(
        guest_id=guest_id,
        event_id=event_id
    ).first()

    # STEP 4: UPSERT logic
    if attendance:
        attendance.status = 'attending'
        attendance.table_number = table_number
        attendance.role = role
    else:
        attendance = Attendance(
            status='attending',
            table_number=table_number,
            role=role,
            guest_id=guest_id,
            event_id=event_id
        )
        db.session.add(attendance)

    db.session.commit()

    return {
        'status': 'success',
        'guest_id': guest_id,
        'event_id': event_id,
        'table_number': table_number
    }

except (ValueError, SQLAlchemyError) as e:
    db.session.rollback()

    return {
        'status': 'error',
        'reason': str(e)
    }, 409

Explanation

This transaction guarantees that RSVP acceptance and attendance assignment happen together atomically. If attendance creation fails after the RSVP update, the rollback restores the RSVP to its previous state, preventing inconsistencies.

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.

Flask Implementation

from sqlalchemy.exc import SQLAlchemyError

def cancel_wedding(wedding_id):
try:
db.session.begin()

    # STEP 1: Find wedding
    wedding = db.session.query(Wedding).filter_by(
        wedding_id=wedding_id
    ).first()

    if not wedding:
        raise ValueError('Wedding not found')

    # STEP 2: Update wedding status
    wedding.notes = 'Wedding cancelled'

    # STEP 3: Cancel venue bookings
    db.session.query(VenueBooking).filter_by(
        wedding_id=wedding_id
    ).update({
        'status': 'cancelled'
    })

    # STEP 4: Cancel photographer bookings
    db.session.query(PhotographerBooking).filter_by(
        wedding_id=wedding_id
    ).update({
        'status': 'cancelled'
    })

    # STEP 5: Cancel band bookings
    db.session.query(BandBooking).filter_by(
        wedding_id=wedding_id
    ).update({
        'status': 'cancelled'
    })

    # STEP 6: Cancel registrar bookings
    db.session.query(RegistrarBooking).filter_by(
        wedding_id=wedding_id
    ).update({
        'status': 'cancelled'
    })

    # STEP 7: Cancel events
    db.session.query(Event).filter_by(
        wedding_id=wedding_id
    ).update({
        'status': 'cancelled'
    })

    # STEP 8: Find all wedding events
    events = db.session.query(Event.event_id).filter_by(
        wedding_id=wedding_id
    ).all()

    event_ids = [e.event_id for e in events]

    # STEP 9: Mark attendance absent
    if event_ids:
        db.session.query(Attendance).filter(
            Attendance.event_id.in_(event_ids)
        ).update({
            'status': 'absent'
        }, synchronize_session=False)

    db.session.commit()

    return {
        'status': 'success',
        'wedding_id': wedding_id
    }

except (ValueError, SQLAlchemyError) as e:
    db.session.rollback()

    return {
        'status': 'error',
        'reason': str(e)
    }, 500

Explanation

This transaction performs a full cancellation cascade across all dependent tables. Every update occurs inside one transaction so the database can never contain partially cancelled wedding data.

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;

Flask Implementation

from sqlalchemy.exc import SQLAlchemyError

def validate_wedding_budget(wedding_id):
try:
db.session.begin()

    # STEP 1: Lock wedding row
    wedding = db.session.query(Wedding)\
        .filter_by(wedding_id=wedding_id)\
        .with_for_update()\
        .first()

    if not wedding:
        raise ValueError('Wedding not found')

    budget = wedding.budget

    # STEP 2: Calculate venue costs
    venue_total = db.session.query(
        db.func.coalesce(db.func.sum(VenueBooking.price), 0)
    ).filter_by(
        wedding_id=wedding_id
    ).scalar()

    # STEP 3: Calculate photographer costs
    photographer_total = db.session.query(
        db.func.coalesce(db.func.sum(PhotographerBooking.price), 0)
    ).filter_by(
        wedding_id=wedding_id
    ).scalar()

    # STEP 4: Calculate band costs
    band_total = db.session.query(
        db.func.coalesce(db.func.sum(BandBooking.price), 0)
    ).filter_by(
        wedding_id=wedding_id
    ).scalar()

    # STEP 5: Calculate registrar costs
    registrar_total = db.session.query(
        db.func.coalesce(db.func.sum(RegistrarBooking.price), 0)
    ).filter_by(
        wedding_id=wedding_id
    ).scalar()

    # STEP 6: Compute total cost
    total_cost = (
        venue_total +
        photographer_total +
        band_total +
        registrar_total
    )

    # STEP 7: Validate budget
    if total_cost > budget:
        raise ValueError(
            f'Budget exceeded. Total: {total_cost}, Budget: {budget}'
        )

    # STEP 8: Finalise wedding
    wedding.notes = 'Budget validated – wedding finalised'

    db.session.commit()

    return {
        'status': 'success',
        'wedding_id': wedding_id,
        'budget': float(budget),
        'total_cost': float(total_cost)
    }

except (ValueError, SQLAlchemyError) as e:
    db.session.rollback()

    return {
        'status': 'error',
        'reason': str(e)
    }, 409

Explanation

The with_for_update() row lock prevents concurrent transactions from modifying the wedding budget or related financial data during validation. This avoids TOCTOU (Time Of Check To Time Of Use) race conditions.

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;

Flask Implementation

from sqlalchemy.exc import SQLAlchemyError

def partial_booking_example(wedding_id, venue_cfg, photo_cfg, band_cfg):
try:
db.session.begin()

    # STEP 1: Insert venue booking
    venue = VenueBooking(
        **venue_cfg,
        wedding_id=wedding_id
    )
    db.session.add(venue)

    # STEP 2: Insert photographer booking
    photographer = PhotographerBooking(
        **photo_cfg,
        wedding_id=wedding_id
    )
    db.session.add(photographer)

    # STEP 3: Create savepoint
    savepoint = db.session.begin_nested()

    try:
        # STEP 4: Attempt risky band booking
        band = BandBooking(
            **band_cfg,
            wedding_id=wedding_id
        )

        db.session.add(band)
        db.session.flush()

    except SQLAlchemyError:
        # STEP 5: Roll back only to savepoint
        savepoint.rollback()

    # STEP 6: Continue transaction
    db.session.commit()

    return {
        'status': 'partial_success'
    }

except SQLAlchemyError as e:
    db.session.rollback()

    return {
        'status': 'error',
        'reason': str(e)
    }, 500

Explanation

begin_nested() creates a database savepoint. If the band booking fails, only the operations after the savepoint are rolled back while earlier successful inserts remain intact.

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.

Last modified 8 days ago Last modified on 05/18/26 21:18:36
Note: See TracWiki for help on using the wiki.