| 8 | | == What we cover in this phase == |
| 9 | | In this phase we focus on: |
| 10 | | |
| 11 | | * Database transactions in real application scenarios |
| 12 | | * Atomic operations across multiple related tables |
| 13 | | * Conflict detection and concurrency handling |
| 14 | | * Isolation levels and race conditions |
| 15 | | * Savepoints for partial rollback logic |
| 16 | | * Connection pooling and performance scaling |
| 17 | | * Flask + SQLAlchemy implementation patterns |
| 18 | | |
| 20 | | |
| 21 | | === Business scenario === |
| 22 | | When a user finalises a wedding plan, all services must be booked atomically: |
| 23 | | venue, photographer, band, and registrar. |
| 24 | | |
| 25 | | If any single booking fails, none of the changes should be saved. |
| 26 | | |
| 27 | | === Tables involved === |
| 28 | | |
| 29 | | * venue_booking (overlap check on venue_id + date) |
| 30 | | * photographer_booking (overlap check on photographer_id + date) |
| 31 | | * band_booking (overlap check on band_id + date) |
| 32 | | * registrar_booking (overlap check on registrar_id + date) |
| 33 | | * wedding (update notes after success) |
| 34 | | |
| 35 | | === Conflict detection logic === |
| 36 | | Two bookings overlap when: |
| 37 | | {{{ |
| 38 | | start_time < new_end_time AND end_time > new_start_time |
| 39 | | }}} |
| 40 | | |
| 41 | | === SQL Transaction === |
| 42 | | {{{ |
| 43 | | BEGIN; |
| 44 | | |
| 45 | | -- Venue check |
| | 9 | === Business Scenario === |
| | 10 | 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. |
| | 11 | |
| | 12 | === Tables Involved === |
| | 13 | • venue_booking – insert + time-overlap check on venue_id + date |
| | 14 | • photographer_booking – insert + overlap check on photographer_id + date |
| | 15 | • band_booking – insert + overlap check on band_id + date |
| | 16 | • registrar_booking – insert + overlap check on registrar_id + date |
| | 17 | • wedding – update notes after all bookings succeed |
| | 18 | |
| | 19 | === Conflict Detection Logic === |
| | 20 | All four booking tables share the same overlap predicate. Two time windows overlap when: |
| | 21 | |
| | 22 | start_time < $new_end_time AND end_time > $new_start_time |
| | 23 | |
| | 24 | === Full Transaction – SQL === |
| | 25 | {{{ |
| | 26 | BEGIN; |
| | 27 | |
| | 28 | ``` |
| | 29 | • STEP 1: Check venue availability |
| | 30 | ``` |
| | 31 | |
| 71 | | WHERE registrar_id = 1 |
| 72 | | AND date = '2026-06-20' |
| 73 | | AND start_time < '10:30' |
| 74 | | AND end_time > '10:00' |
| 75 | | AND status <> 'cancelled'; |
| 76 | | |
| 77 | | -- Inserts |
| 78 | | INSERT INTO project.venue_booking (...) |
| | 67 | WHERE registrar_id = 1 |
| | 68 | AND date = '2026-06-20' |
| | 69 | AND start_time < '10:30' |
| | 70 | AND end_time > '10:00' |
| | 71 | AND status <> 'cancelled'; |
| | 72 | |
| | 73 | ``` |
| | 74 | • STEP 5: Insert all four bookings |
| | 75 | ``` |
| | 76 | |
| | 77 | INSERT INTO project.venue_booking |
| | 78 | (date, start_time, end_time, status, price, venue_id, wedding_id) |
| 92 | | SET notes = 'All services confirmed' |
| 93 | | WHERE wedding_id = 1; |
| 94 | | |
| 95 | | COMMIT; |
| 96 | | }}} |
| 97 | | |
| 98 | | === Flask implementation === |
| 99 | | {{{ |
| 100 | | def book_full_wedding(...): |
| | 95 | SET notes = 'All services confirmed' |
| | 96 | WHERE wedding_id = 1; |
| | 97 | |
| | 98 | COMMIT; |
| | 99 | -- On any error at any step: ROLLBACK; |
| | 100 | }}} |
| | 101 | |
| | 102 | === Flask Implementation (SQLAlchemy & Python) === |
| | 103 | {{{ |
| | 104 | from sqlalchemy.exc import SQLAlchemyError |
| | 105 | |
| | 106 | def book_full_wedding(wedding_id, venue_cfg, photo_cfg, band_cfg, reg_cfg): |
| 105 | | _check_overlap(...) |
| 106 | | db.session.add(...) |
| | 111 | # Conflict checks – raise ValueError if any overlap found |
| | 112 | _check_overlap('venue_booking', 'venue_id', venue_cfg) |
| | 113 | _check_overlap('photographer_booking', 'photographer_id', photo_cfg) |
| | 114 | _check_overlap('band_booking', 'band_id', band_cfg) |
| | 115 | _check_overlap('registrar_booking', 'registrar_id', reg_cfg) |
| | 116 | |
| | 117 | # Inserts |
| | 118 | db.session.add(VenueBooking(**venue_cfg, wedding_id=wedding_id)) |
| | 119 | db.session.add(PhotographerBooking(**photo_cfg, wedding_id=wedding_id)) |
| | 120 | db.session.add(BandBooking(**band_cfg, wedding_id=wedding_id)) |
| | 121 | db.session.add(RegistrarBooking(**reg_cfg, wedding_id=wedding_id)) |
| | 122 | |
| | 123 | db.session.query(Wedding).filter_by(wedding_id=wedding_id)\ |
| | 124 | .update({'notes': 'All services confirmed'}) |
| | 125 | |
| 111 | | ``` |
| 112 | | |
| 113 | | }}} |
| 114 | | |
| 115 | | == Transaction 2 – Guest Registration with RSVP Seeding == |
| 116 | | |
| 117 | | === Business scenario === |
| 118 | | When a guest is added, they must automatically receive a pending RSVP for every event in the wedding. |
| 119 | | |
| 120 | | === Tables involved === |
| 121 | | |
| 122 | | * guest (insert) |
| 123 | | * event (read all events) |
| 124 | | * event_rsvp (insert pending rows) |
| 125 | | |
| 126 | | === Why transaction is needed === |
| 127 | | Without a transaction, a system crash could leave: |
| 128 | | |
| 129 | | * guest inserted |
| 130 | | * but missing RSVP records → inconsistent state |
| 131 | | |
| 132 | | === SQL Transaction === |
| 133 | | {{{ |
| 134 | | BEGIN; |
| 135 | | |
| 136 | | INSERT INTO project.guest (...) |
| 137 | | VALUES ('Marija','Ilievska','[marija@gmail.com](mailto:marija@gmail.com)',1) |
| | 131 | return {'status': 'error', 'reason': str(e)}, 409 |
| | 132 | ``` |
| | 133 | |
| | 134 | }}} |
| | 135 | |
| | 136 | == Transaction 2 – Guest Registration with Automatic RSVP Seeding == |
| | 137 | === Business Scenario === |
| | 138 | 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. |
| | 139 | |
| | 140 | === Tables Involved === |
| | 141 | • guest – insert the new guest record |
| | 142 | • event – read all events for this wedding_id |
| | 143 | • event_rsvp – insert one pending RSVP per event (enforced by uq_rsvp: guest_id + event_id) |
| | 144 | |
| | 145 | === Why a Transaction is Required === |
| | 146 | 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. |
| | 147 | |
| | 148 | === Full Transaction – SQL === |
| | 149 | {{{ |
| | 150 | BEGIN; |
| | 151 | |
| | 152 | ``` |
| | 153 | • STEP 1: Insert new guest |
| | 154 | ``` |
| | 155 | |
| | 156 | INSERT INTO project.guest (first_name, last_name, email, wedding_id) |
| | 157 | VALUES ('Marija', 'Ilievska', '[marija@gmail.com](mailto:marija@gmail.com)', 1) |
| 152 | | guest = Guest(...) |
| 153 | | db.session.add(guest) |
| 154 | | db.session.flush() |
| 155 | | |
| 156 | | events = db.session.query(Event).filter_by(wedding_id=wedding_id).all() |
| 157 | | |
| 158 | | for event in events: |
| 159 | | db.session.add(EventRSVP(...)) |
| 160 | | |
| 161 | | db.session.commit() |
| 162 | | }}} |
| 163 | | |
| 164 | | Key idea: flush() is used to obtain guest_id before commit. |
| | 183 | ``` |
| | 184 | guest = Guest(first_name=first_name, last_name=last_name, |
| | 185 | email=email, wedding_id=wedding_id) |
| | 186 | db.session.add(guest) |
| | 187 | db.session.flush() |
| | 188 | |
| | 189 | events = db.session.query(Event)\ |
| | 190 | .filter_by(wedding_id=wedding_id).all() |
| | 191 | |
| | 192 | for event in events: |
| | 193 | rsvp = EventRSVP( |
| | 194 | status='pending', |
| | 195 | response_date=date.today(), |
| | 196 | guest_id=guest.guest_id, |
| | 197 | event_id=event.event_id |
| | 198 | ) |
| | 199 | db.session.add(rsvp) |
| | 200 | |
| | 201 | db.session.commit() |
| | 202 | return {'guest_id': guest.guest_id, 'rsvps_created': len(events)} |
| | 203 | |
| | 204 | except SQLAlchemyError as e: |
| | 205 | db.session.rollback() |
| | 206 | return {'error': str(e)}, 500 |
| | 207 | ``` |
| | 208 | |
| | 209 | }}} |
| | 210 | |
| | 211 | 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. |
| 167 | | |
| 168 | | === Business scenario === |
| 169 | | When a guest accepts an RSVP: |
| 170 | | |
| 171 | | * RSVP status updates to accepted |
| 172 | | * Attendance row is created or updated |
| 173 | | |
| 174 | | Both must happen together. |
| 175 | | |
| 176 | | === Tables involved === |
| 177 | | |
| 178 | | * event_rsvp (update) |
| 179 | | * attendance (UPSERT) |
| 180 | | |
| 181 | | === SQL Transaction === |
| 182 | | {{{ |
| 183 | | BEGIN; |
| | 214 | === Business Scenario === |
| | 215 | 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. |
| | 216 | |
| | 217 | === Tables Involved === |
| | 218 | • event_rsvp – update status from pending to accepted |
| | 219 | • attendance – UPSERT the row for this guest_id + event_id using the uq_attendance constraint |
| | 220 | |
| | 221 | === Full Transaction === |
| | 222 | {{{ |
| | 223 | BEGIN; |
| | 224 | |
| | 225 | ``` |
| | 226 | • STEP 1: Update RSVP status |
| | 227 | ``` |
| 188 | | WHERE guest_id = 1 AND event_id = 2; |
| 189 | | |
| 190 | | INSERT INTO project.attendance (...) |
| 191 | | VALUES ('attending', 5, 'Guest', 1, 2) |
| | 232 | WHERE guest_id = 1 |
| | 233 | AND event_id = 2; |
| | 234 | else ROLLBACK |
| | 235 | |
| | 236 | ``` |
| | 237 | • STEP 2: Upsert attendance record |
| | 238 | ``` |
| | 239 | |
| | 240 | INSERT INTO project.attendance |
| | 241 | (status, table_number, role, guest_id, event_id) |
| | 242 | VALUES |
| | 243 | ('attending', 5, 'Guest', 1, 2) |
| 196 | | role = EXCLUDED.role; |
| 197 | | |
| 198 | | COMMIT; |
| 199 | | }}} |
| 200 | | |
| 201 | | === Key concept === |
| 202 | | ON CONFLICT prevents race conditions and ensures atomic seat assignment. |
| 203 | | |
| 204 | | == Transaction 4 – Wedding Cancellation with Cascade == |
| 205 | | |
| 206 | | === Business scenario === |
| 207 | | Cancelling a wedding must cascade across: |
| 208 | | |
| 209 | | * bookings |
| 210 | | * events |
| 211 | | * attendance records |
| 212 | | |
| 213 | | Partial cancellation is not allowed. |
| 214 | | |
| 215 | | === Tables involved === |
| 216 | | |
| 217 | | * wedding |
| 218 | | * venue_booking |
| 219 | | * photographer_booking |
| 220 | | * band_booking |
| 221 | | * registrar_booking |
| 222 | | * event |
| 223 | | * attendance |
| 224 | | |
| 225 | | === SQL Transaction === |
| | 248 | role = EXCLUDED.role; |
| | 249 | |
| | 250 | COMMIT; |
| | 251 | -- Error: ROLLBACK; |
| | 252 | }}} |
| | 253 | |
| | 254 | === Key pattern === |
| | 255 | 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. |
| | 256 | |
| | 257 | == Transaction 4 – Wedding Cancellation with Status Cascade == |
| | 258 | === Business Scenario === |
| | 259 | 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. |
| | 260 | |
| | 261 | === Tables Involved === |
| | 262 | • wedding – update status/notes |
| | 263 | • venue_booking – cancel bookings |
| | 264 | • photographer_booking – cancel bookings |
| | 265 | • band_booking – cancel bookings |
| | 266 | • registrar_booking – cancel bookings |
| | 267 | • event – cancel events |
| | 268 | • attendance – mark attendance absent |
| | 269 | |
| | 270 | === Full Transaction – SQL === |
| 230 | | SET notes = 'Wedding cancelled' |
| 231 | | WHERE wedding_id = 1; |
| 232 | | |
| 233 | | UPDATE project.venue_booking SET status='cancelled' WHERE wedding_id=1; |
| 234 | | UPDATE project.photographer_booking SET status='cancelled' WHERE wedding_id=1; |
| 235 | | UPDATE project.band_booking SET status='cancelled' WHERE wedding_id=1; |
| 236 | | UPDATE project.registrar_booking SET status='cancelled' WHERE wedding_id=1; |
| 237 | | |
| 238 | | UPDATE project.event SET status='cancelled' WHERE wedding_id=1; |
| | 275 | SET notes = 'Wedding cancelled' |
| | 276 | WHERE wedding_id = 1; |
| | 277 | |
| | 278 | UPDATE project.venue_booking |
| | 279 | SET status = 'cancelled' |
| | 280 | WHERE wedding_id = 1; |
| | 281 | |
| | 282 | UPDATE project.photographer_booking |
| | 283 | SET status = 'cancelled' |
| | 284 | WHERE wedding_id = 1; |
| | 285 | |
| | 286 | UPDATE project.band_booking |
| | 287 | SET status = 'cancelled' |
| | 288 | WHERE wedding_id = 1; |
| | 289 | |
| | 290 | UPDATE project.registrar_booking |
| | 291 | SET status = 'cancelled' |
| | 292 | WHERE wedding_id = 1; |
| | 293 | |
| | 294 | UPDATE project.event |
| | 295 | SET status = 'cancelled' |
| | 296 | WHERE wedding_id = 1; |
| 253 | | |
| 254 | | === Business scenario === |
| 255 | | Before finalising a wedding, ensure total cost does not exceed budget. |
| 256 | | |
| 257 | | Must prevent race conditions where new bookings are inserted during validation. |
| 258 | | |
| 259 | | === Tables involved === |
| 260 | | |
| 261 | | * wedding (FOR UPDATE lock) |
| 262 | | * venue_booking |
| 263 | | * photographer_booking |
| 264 | | * band_booking |
| 265 | | * registrar_booking |
| 266 | | |
| 267 | | === SQL Transaction === |
| | 313 | === Business Scenario === |
| | 314 | Before finalising a wedding, the system must ensure total cost does not exceed budget. The check and update must be atomic. |
| | 315 | |
| | 316 | === Tables Involved === |
| | 317 | • wedding – locked using FOR UPDATE |
| | 318 | • venue_booking |
| | 319 | • photographer_booking |
| | 320 | • band_booking |
| | 321 | • registrar_booking |
| | 322 | |
| | 323 | === Full Transaction – SQL === |
| 326 | | === Levels used in this system === |
| 327 | | |
| 328 | | * READ COMMITTED – simple reads (guest lists, catalog) |
| 329 | | * REPEATABLE READ – RSVP operations |
| 330 | | * SERIALIZABLE – full booking and budget validation |
| 331 | | |
| 332 | | === SQLAlchemy setting === |
| | 378 | === Levels === |
| | 379 | |
| | 380 | * READ COMMITTED – basic reads |
| | 381 | * REPEATABLE READ – RSVP consistency |
| | 382 | * SERIALIZABLE – full booking + budget validation |
| | 383 | |
| | 384 | === SQLAlchemy === |
| 338 | | |
| 339 | | === Why pooling is needed === |
| 340 | | Without pooling: |
| 341 | | |
| 342 | | * each request opens a new DB connection |
| 343 | | * high overhead and slow response |
| 344 | | |
| 345 | | With pooling: |
| 346 | | |
| 347 | | * connections are reused |
| 348 | | * better scalability and stability |
| 349 | | |
| 350 | | === Key endpoints affected === |
| 351 | | |
| 352 | | * /availability/* |
| 353 | | * /rsvp |
| 354 | | * /weddings/<id>/book-all |
| 355 | | * /guests |
| 356 | | |
| 357 | | === Pool configuration === |
| | 390 | === Why needed === |
| | 391 | Pooling reuses DB connections instead of creating new ones per request. |
| | 392 | |
| | 393 | === Affected endpoints === |
| | 394 | |
| | 395 | * availability endpoints |
| | 396 | * RSVP endpoints |
| | 397 | * booking endpoints |
| | 398 | |
| | 399 | === Configuration === |
| 378 | | This phase introduced advanced database reliability and scalability concepts applied directly to the Wedding Planner system. |
| 379 | | |
| 380 | | Key outcomes: |
| 381 | | |
| 382 | | * Transactions ensure atomic multi-table operations |
| 383 | | * Concurrency control prevents race conditions and inconsistencies |
| 384 | | * Isolation levels protect against anomalies |
| 385 | | * Savepoints allow partial rollback for optional features |
| 386 | | * Connection pooling improves performance under load |
| 387 | | |
| 388 | | These mechanisms are essential for production-grade backend systems handling concurrent users and complex relational workflows. |
| | 420 | This phase introduced five full, schema-specific transactions for the Wedding Planner system: |
| | 421 | |
| | 422 | * Full wedding booking with atomic service allocation |
| | 423 | * Guest registration with RSVP seeding |
| | 424 | * RSVP acceptance with attendance UPSERT |
| | 425 | * Wedding cancellation with full cascade |
| | 426 | * Budget validation with row locking |
| | 427 | |
| | 428 | These ensure ACID compliance, prevent race conditions, and guarantee consistent multi-table operations in a concurrent environment. |