Module 0 · Getting set up

Lesson 00b — TicketFlow initial modeling

From the business to the invariants and from there to the schema: constraints that make selling a seat twice impossible.

Published
In this lesson
  1. Exercise 1 — Implement the v1 model
  2. Exercise 2 — Prove invariant I1 in the shell
  3. Exercise 3 — Availability query
  4. Exercise 4 — State machine in the model
  5. Exercise 5 — Design what v1 left out (discussion)
  6. Exercise 6 — Senior reflection (5 minutes)
  7. Submission

The only way to learn modeling is by modeling. Implement the v1 model and then tackle the challenges. Don't look at solutions.md until you've submitted.

Setup:

bash
cd ~/dev/ticketflow && source .venv/bin/activate

Exercise 1 — Implement the v1 model

Create in events/models.py the classes from the lesson: TimeStampedModel, User, Event, Seat, Reservation, ReservationItem and Payment (with their TextChoices).

Register the app in settings.py (INSTALLED_APPS), and declare in settings.py:

python
AUTH_USER_MODEL = "events.User"

Do this before the first migration. Changing AUTH_USER_MODEL mid-project is one of Django's most painful migrations; that's why we decide it now (Lesson 11 pulled forward: schema decisions are made before data exists).

Then run:

bash
python manage.py makemigrations
python manage.py migrate

Deliverable: the list of models python manage.py makemigrations --dry-run -v2 prints (or simply confirm it migrated without errors and paste the summary).

Exercise 2 — Prove invariant I1 in the shell

Open python manage.py shell and try to break the invariant: two active reservations with the same seat.

python
from events.models import *

u1 = User.objects.create(email="ana@example.com")
u2 = User.objects.create(email="beto@example.com")
ev = Event.objects.create(title="Concert", venue="Hall X", starts_at="2027-03-01T21:00:00Z", state=EventState.PUBLISHED, organizer=u1)
s1 = Seat.objects.create(event=ev, row="A", number=1)
s2 = Seat.objects.create(event=ev, row="A", number=2)

r1 = Reservation.objects.create(user=u1, event=ev, status=ReservationState.PENDING_PAYMENT, expires_at="2026-10-01T12:00:00Z")
ReservationItem.objects.create(reservation=r1, seat=s1, price_at_purchase="50.00")

r2 = Reservation.objects.create(user=u2, event=ev, status=ReservationState.PENDING_PAYMENT, expires_at="2026-10-01T12:00:00Z")
ReservationItem.objects.create(reservation=r2, seat=s1, price_at_purchase="50.00")   # ← should this explode?
  1. Which exception does the last line raise, and with what message?
  2. Temporarily remove the two conditional UniqueConstraints from the model, remake the migration and repeat the experiment. Does the second reservation get created? What does that mean for the business?
  3. Restore the constraints. Write down in 2 lines what you just demonstrated (this is the proof you'll tell in an interview).

Exercise 3 — Availability query

Write (in the shell, or in events/services.py if you're brave) a function available_seats(event_id) that returns the event's seats without an active or confirmed reservation.

  1. Write it without raw SQL (hint: subquery or exclude over the reverse relation).
  2. Print the number of queries it runs with django.db.connection.queries or django-debug-toolbar… or simply len(connection.queries) after calling it twice.
  3. Does it run 1 query or N? (Keep your answer: we'll revisit it in Lesson 09 with EXPLAIN.)

Exercise 4 — State machine in the model

Add to Reservation the methods with legal transitions:

python
def confirm(self, when):
    ...
def cancel(self, when):
    ...

Rules: only a PENDING_PAYMENT reservation with an unexpired expires_at and a SUCCEEDED payment can be confirmed; only a non-cancelled reservation can be cancelled (cancelling something cancelled must be idempotent: it doesn't fail, it changes nothing).

  1. Implement both methods raising ValueError (or a domain exception of your own) on illegal transitions.
  2. In the shell: create an expired reservation (expires_at in the past) and try confirm(). What happens?
  3. Run cancel() twice. Is the second a silent no-op or does it raise? Justify your choice.

Exercise 5 — Design what v1 left out (discussion)

Answer reasoning about trade-offs (2-3 sentences each, no code needed):

  1. The organizer wants "the whole VIP row A" as a separate product with a different price. What would you change in the model? Is it worth it for v1?
  2. TicketFlow wants to allow "up to 4 seats per user per event". Where does that rule live: a DB constraint, a serializer validation, or a model method? Why?
  3. A competitor sells "general admission tickets without an assigned seat" (capacity only). Would you reuse Seat or model a different ticket type? What does each option break?

Exercise 6 — Senior reflection (5 minutes)

Imagine that in 6 months TicketFlow has 500 events and 2M seats. With the v1 model:

  1. Which query worries you the most? (hint: the one everyone makes every second)
  2. Which index or table do you feel will hurt first?
  3. What would you do today — cheap to do now, very expensive later — to prepare?

Submission

Paste your results in the chat (code, exceptions and answers). I'll grade them one by one, and with that we close 00b and mark it mastered. Next: Lesson 01 — HTTP in depth if you haven't done it, or Lesson 02 — Networking if you already submitted 01.