| 11 | | * What a database transaction is and why it is needed |
| 12 | | * COMMIT vs ROLLBACK |
| 13 | | * Isolation and concurrency problems (dirty reads, lost updates, etc.) |
| 14 | | * How transactions apply to our Wedding Planner scenarios (bookings, RSVP, conflicts) |
| 15 | | * What connection pooling is and why it improves performance |
| 16 | | * How pooling would be used in a real version of our application |
| 17 | | |
| 18 | | == Transactions in databases == |
| 19 | | |
| 20 | | === What is a transaction? === |
| 21 | | A database transaction is a group of operations that must execute as a single unit. |
| 22 | | The transaction is successful only if all operations succeed. |
| 23 | | |
| 24 | | Transactions are needed whenever multiple inserts/updates must happen together, without leaving the database in an inconsistent state. |
| 25 | | |
| 26 | | === Why are transactions important? (ACID) === |
| 27 | | Transactions ensure: |
| 28 | | * Atomicity – all operations succeed or none are applied |
| 29 | | * Consistency – data constraints remain valid |
| 30 | | * Isolation – concurrent operations do not corrupt data |
| 31 | | * Durability – once committed, changes remain stored |
| 32 | | |
| 33 | | This is commonly summarized as the ACID principles. |
| 34 | | |
| 35 | | === COMMIT and ROLLBACK === |
| 36 | | A transaction typically works like this: |
| 37 | | * BEGIN – start transaction |
| 38 | | * Execute multiple SQL operations |
| 39 | | * COMMIT – save changes permanently if everything succeeded |
| 40 | | * ROLLBACK – undo everything if an error occurs |
| 41 | | |
| 42 | | Example (SQL pseudocode): |
| 43 | | |
| 44 | | {{{ |
| 45 | | BEGIN; |
| 46 | | |
| 47 | | UPDATE venue_booking SET status='CONFIRMED' WHERE booking_id=10; |
| 48 | | UPDATE wedding SET notes='Venue confirmed' WHERE wedding_id=1; |
| 49 | | |
| 50 | | COMMIT; |
| 51 | | }}} |
| 52 | | |
| 53 | | If an error happens during the process, the correct behavior is: |
| 54 | | |
| 55 | | {{{ |
| 56 | | ROLLBACK; |
| 57 | | }}} |
| 58 | | |
| 59 | | === Transaction example connected to our project === |
| 60 | | |
| 61 | | ==== Scenario: Venue booking confirmation ==== |
| 62 | | In the Wedding Planner application, when the user books a venue we must: |
| 63 | | * insert the booking record |
| 64 | | * update wedding information |
| 65 | | * ensure there is no overlapping booking for the same venue |
| 66 | | |
| 67 | | If one step fails, the database must not store partial data. |
| 68 | | |
| 69 | | Pseudocode logic: |
| 70 | | |
| 71 | | {{{ |
| 72 | | BEGIN TRANSACTION |
| 73 | | |
| 74 | | Check if venue is free at that time |
| 75 | | |
| 76 | | Insert venue_booking |
| 77 | | |
| 78 | | Update wedding record |
| 79 | | |
| 80 | | COMMIT |
| 81 | | If any step fails -> ROLLBACK |
| 82 | | }}} |
| 83 | | |
| 84 | | ==== Conflict detection and why transaction is needed ==== |
| 85 | | In our prototype we check conflicts using the endpoint: |
| 86 | | |
| 87 | | {{{ |
| 88 | | /availability/venue?venue_id=1&date=2026-06-20&start=15:00&end=21:00 |
| 89 | | }}} |
| 90 | | |
| 91 | | In a real system, the conflict check MUST be done inside a transaction, because two users could do this at the same time: |
| 92 | | |
| 93 | | * User A checks availability → free |
| 94 | | * User B checks availability → free |
| 95 | | * Both insert a booking at the same moment |
| 96 | | * Result → double-booking |
| 97 | | |
| 98 | | Correct approach: |
| 99 | | * conflict check + insert booking must execute atomically inside one transaction. |
| 100 | | |
| 101 | | == Isolation and concurrency (basic overview) == |
| 102 | | |
| 103 | | === What is isolation? === |
| 104 | | Isolation defines how multiple users/requests interact with the database at the same time. |
| 105 | | If isolation is weak, concurrent transactions may read or write inconsistent values. |
| 106 | | |
| 107 | | Common concurrency problems: |
| 108 | | * Dirty read – reading uncommitted changes |
| 109 | | * Non-repeatable read – the same query returns different result inside one transaction |
| 110 | | * Phantom read – new rows appear in repeated query |
| 111 | | * Lost update – two users overwrite each other’s updates |
| 112 | | |
| 113 | | Even though SQLite is simplified, production systems must carefully choose the isolation level. |
| 114 | | |
| 115 | | === Example connected to our project === |
| 116 | | If multiple users RSVP or update attendance at the same time without transaction + isolation: |
| 117 | | * Attendance table could end up with duplicates |
| 118 | | * RSVP results could be overwritten |
| 119 | | * Counts per event may become incorrect |
| 120 | | |
| 121 | | Therefore, operations such as RSVP update and attendance update should be handled transactionally. |
| 122 | | |
| 123 | | == Transactions inside Flask prototype == |
| 124 | | |
| 125 | | === How transactions would be handled in Flask === |
| 126 | | In Flask applications (SQLAlchemy or DB sessions), the usual pattern is: |
| 127 | | * start transaction |
| 128 | | * execute queries |
| 129 | | * commit |
| 130 | | * if error → rollback |
| 131 | | |
| 132 | | Pseudocode example: |
| 133 | | |
| 134 | | {{{ |
| | 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 | |
| | 19 | == Transaction 1 – Full Wedding Bundle Booking == |
| | 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 |
| | 46 | SELECT 1 FROM project.venue_booking |
| | 47 | WHERE venue_id = 1 |
| | 48 | AND date = '2026-06-20' |
| | 49 | AND start_time < '23:00' |
| | 50 | AND end_time > '16:00' |
| | 51 | AND status <> 'cancelled'; |
| | 52 | |
| | 53 | -- Photographer check |
| | 54 | SELECT 1 FROM project.photographer_booking |
| | 55 | WHERE photographer_id = 1 |
| | 56 | AND date = '2026-06-20' |
| | 57 | AND start_time < '22:00' |
| | 58 | AND end_time > '14:00' |
| | 59 | AND status <> 'cancelled'; |
| | 60 | |
| | 61 | -- Band check |
| | 62 | SELECT 1 FROM project.band_booking |
| | 63 | WHERE band_id = 1 |
| | 64 | AND date = '2026-06-20' |
| | 65 | AND start_time < '23:00' |
| | 66 | AND end_time > '18:00' |
| | 67 | AND status <> 'cancelled'; |
| | 68 | |
| | 69 | -- Registrar check |
| | 70 | SELECT 1 FROM project.registrar_booking |
| | 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 (...) |
| | 79 | VALUES ('2026-06-20','16:00','23:00','confirmed',7000,1,1); |
| | 80 | |
| | 81 | INSERT INTO project.photographer_booking (...) |
| | 82 | VALUES ('2026-06-20','14:00','22:00','confirmed',1,1); |
| | 83 | |
| | 84 | INSERT INTO project.band_booking (...) |
| | 85 | VALUES ('2026-06-20','18:00','23:00','confirmed',1,1); |
| | 86 | |
| | 87 | INSERT INTO project.registrar_booking (...) |
| | 88 | VALUES ('2026-06-20','10:00','10:30','confirmed',1,1); |
| | 89 | |
| | 90 | -- Update wedding |
| | 91 | UPDATE project.wedding |
| | 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(...): |
| 145 | | }}} |
| 146 | | |
| 147 | | Even though our prototype is minimal and uses SQLite, the concept stays identical and scales directly to PostgreSQL. |
| 148 | | |
| 149 | | == Database connection pooling == |
| 150 | | |
| 151 | | === What is connection pooling? === |
| 152 | | Connection pooling means reusing open database connections instead of creating a new connection for every request. |
| 153 | | |
| | 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) |
| | 138 | RETURNING guest_id; |
| | 139 | |
| | 140 | INSERT INTO project.event_rsvp (...) |
| | 141 | SELECT 'pending', CURRENT_DATE, :new_guest_id, e.event_id |
| | 142 | FROM project.event e |
| | 143 | WHERE e.wedding_id = 1; |
| | 144 | |
| | 145 | COMMIT; |
| | 146 | }}} |
| | 147 | |
| | 148 | === Flask implementation === |
| | 149 | {{{ |
| | 150 | db.session.begin() |
| | 151 | |
| | 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. |
| | 165 | |
| | 166 | == Transaction 3 – RSVP Acceptance and Seat Assignment == |
| | 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; |
| | 184 | |
| | 185 | UPDATE project.event_rsvp |
| | 186 | SET status = 'accepted', |
| | 187 | response_date = CURRENT_DATE |
| | 188 | WHERE guest_id = 1 AND event_id = 2; |
| | 189 | |
| | 190 | INSERT INTO project.attendance (...) |
| | 191 | VALUES ('attending', 5, 'Guest', 1, 2) |
| | 192 | ON CONFLICT (guest_id, event_id) |
| | 193 | DO UPDATE SET |
| | 194 | status = EXCLUDED.status, |
| | 195 | table_number = EXCLUDED.table_number, |
| | 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 === |
| | 226 | {{{ |
| | 227 | BEGIN; |
| | 228 | |
| | 229 | UPDATE project.wedding |
| | 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; |
| | 239 | |
| | 240 | UPDATE project.attendance |
| | 241 | SET status='absent' |
| | 242 | WHERE event_id IN ( |
| | 243 | SELECT event_id FROM project.event WHERE wedding_id = 1 |
| | 244 | ); |
| | 245 | |
| | 246 | COMMIT; |
| | 247 | }}} |
| | 248 | |
| | 249 | === Concurrency risk === |
| | 250 | Without a transaction, new RSVP updates could be inserted during cancellation, causing inconsistent state. |
| | 251 | |
| | 252 | == Transaction 5 – Budget Validation with Row Locking == |
| | 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 === |
| | 268 | {{{ |
| | 269 | BEGIN; |
| | 270 | |
| | 271 | SELECT budget FROM project.wedding |
| | 272 | WHERE wedding_id = 1 |
| | 273 | FOR UPDATE; |
| | 274 | |
| | 275 | -- Compute total cost (venue + photographer + band + registrar) |
| | 276 | |
| | 277 | IF total_cost > budget THEN |
| | 278 | ROLLBACK; |
| | 279 | END IF; |
| | 280 | |
| | 281 | UPDATE project.wedding |
| | 282 | SET notes = 'Budget validated – wedding finalised' |
| | 283 | WHERE wedding_id = 1; |
| | 284 | |
| | 285 | COMMIT; |
| | 286 | }}} |
| | 287 | |
| | 288 | === Why FOR UPDATE matters === |
| | 289 | It prevents TOCTOU (Time-of-check vs Time-of-use) race conditions by locking the wedding row. |
| | 290 | |
| | 291 | == Savepoints – Partial Rollback == |
| | 292 | |
| | 293 | === Concept === |
| | 294 | Savepoints allow partial rollback inside a transaction without cancelling everything. |
| | 295 | |
| | 296 | === Example === |
| | 297 | {{{ |
| | 298 | BEGIN; |
| | 299 | |
| | 300 | INSERT venue_booking; |
| | 301 | INSERT photographer_booking; |
| | 302 | |
| | 303 | SAVEPOINT after_core; |
| | 304 | |
| | 305 | INSERT band_booking; |
| | 306 | |
| | 307 | ROLLBACK TO SAVEPOINT after_core; |
| | 308 | |
| | 309 | COMMIT; |
| | 310 | }}} |
| | 311 | |
| | 312 | Core bookings remain, optional upgrade is discarded safely. |
| | 313 | |
| | 314 | == Isolation Levels == |
| | 315 | |
| | 316 | === Overview === |
| | 317 | Isolation controls how concurrent transactions interact. |
| | 318 | |
| | 319 | Common problems: |
| | 320 | |
| | 321 | * Dirty reads |
| | 322 | * Non-repeatable reads |
| | 323 | * Phantom reads |
| | 324 | * Lost updates |
| | 325 | |
| | 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 === |
| | 333 | {{{ |
| | 334 | db.session.execute(text('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE')) |
| | 335 | }}} |
| | 336 | |
| | 337 | == Connection Pooling == |
| | 338 | |
| | 339 | === Why pooling is needed === |