Fix a Django Ticketing Order Confirmation Flow: Holds, Seat Counts and Redis

Quick Overview

A project-debugging task on a Django ticketing service: fix the order confirmation flow so held tickets become sold, seat counts stay correct, expired holds are rejected, Redis hold data is cleaned up and sold-out status is set. It tests transactions, row locking, idempotency, time zones and tests.

Fix a Django Ticketing Order Confirmation Flow: Holds, Seat Counts and Redis

Company: Goldman Sachs

Role: Software Engineer

Category: Software Engineering Fundamentals

Difficulty: medium

Interview Round: Online Assessment

This task comes from the project-debugging section of an online assessment. You are given an existing Django ticketing project with documentation and a test suite. A customer first places a hold on tickets for an event, then confirms an order to buy them. The confirm-order flow does not behave as documented; fix it so that it does. The flow covers five concerns: updating ticket status, keeping the event's seat count right, rejecting expired holds, cleaning up the hold's data in Redis, and deciding when the event is sold out. The project's code and documentation were not reported. The models, documented rules and current implementation below are an illustrative stand-in so the task can be practised; treat them as the codebase you were handed. ```python # models.py (illustrative) from django.conf import settings from django.db import models class Event(models.Model): name = models.CharField(max_length=200) capacity = models.PositiveIntegerField() seats_sold = models.PositiveIntegerField(default=0) is_sold_out = models.BooleanField(default=False) class Hold(models.Model): event = models.ForeignKey(Event, on_delete=models.CASCADE) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE) expires_at = models.DateTimeField() released = models.BooleanField(default=False) class Ticket(models.Model): class Status(models.TextChoices): AVAILABLE = "available" HELD = "held" SOLD = "sold" event = models.ForeignKey(Event, on_delete=models.CASCADE) seat = models.CharField(max_length=20) status = models.CharField(max_length=10, choices=Status.choices, default=Status.AVAILABLE) hold = models.ForeignKey(Hold, null=True, blank=True, on_delete=models.SET_NULL) class Order(models.Model): hold = models.OneToOneField(Hold, on_delete=models.PROTECT) user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) created_at = models.DateTimeField(auto_now_add=True) ``` Documented behaviour (illustrative): 1. Only the customer who placed a hold can confirm it, and only while it is unexpired and not released. A hold whose `expires_at` is at or before the server's current time has expired. 2. Confirming changes every ticket in the hold from held to sold and creates exactly one order for the hold. 3. `Event.seats_sold` increases by the number of tickets sold. 4. Placing a hold writes a Redis key `hold:{hold_id}` (the hold's ticket ids, with a time-to-live) and adds the hold's ticket count to a counter `event:{event_id}:held`. Confirming deletes the hold key and subtracts the hold's ticket count from the counter. 5. The event is sold out once `seats_sold` reaches `capacity`. 6. Confirming a hold that already has an order returns that order and changes nothing. ```python # services.py (illustrative): the current implementation from datetime import datetime from .cache import redis_client from .models import Hold, Order, Ticket class HoldExpired(Exception): pass def confirm_order(hold_id, user): hold = Hold.objects.get(pk=hold_id) if hold.expires_at < datetime.now(): raise HoldExpired(hold_id) tickets = Ticket.objects.filter(hold=hold) for ticket in tickets: ticket.status = Ticket.Status.SOLD event = hold.event event.seats_sold += 1 event.save() order = Order.objects.create(hold=hold, user=user) redis_client.delete(f"hold:{hold_id}") if event.seats_sold > event.capacity: event.is_sold_out = True event.save() return order ``` Find every place where `confirm_order` breaks the documented behaviour, fix it, and use the test suite, extended wherever a rule is untested, to show that each fix works. ```hint Rule by rule Put each documented rule next to the line that is supposed to enforce it. Look both for rules that no line enforces and for lines that do something slightly different from their rule. ``` ```hint Two stores, one outcome The database and Redis cannot commit together. Decide which of the two is the source of truth, and at what moment the Redis cleanup is allowed to run. ``` ### Constraints and Clarifications - In the stand-in project, settings use `USE_TZ = True`, so `expires_at` values are timezone-aware. - Several customers can confirm holds for the same event at the same moment, and a client may retry a confirmation after a timeout. ### Clarifying Questions - Is the database or Redis the source of truth for which seats are held, and what else reads the `event:{event_id}:held` counter? - When a confirmation arrives for an expired hold, should it release the hold's tickets immediately, or leave that to the job that expires holds? - Should a repeated confirmation return the existing order even if the hold has expired since the first one? - Which database runs in production and in the tests, and does it support row-level locks? ### What a Strong Answer Covers - Maps every documented rule to the code and names each defect together with the failure it causes - A timezone-aware expiry check with the documented boundary, plus ownership and released-hold checks - Ticket status changes that are saved and limited to held tickets, and a seat count that adds the number of tickets sold without lost updates under concurrency - One atomic transaction with suitable locking, an idempotent retry path, and a correct sold-out condition - Redis cleanup that runs only after commit, tolerates Redis failures and keeps the held counter consistent - Tests that fail before each fix and pass after it, including boundary and retry cases ### Follow-up Questions - Two requests confirm the same hold at the same moment. Walk through what your version does for each. - Redis is unavailable when an order is confirmed. What does the customer see, and how is the stale hold data cleaned up? - The job that expires holds runs while a confirmation of the same hold is in flight. Which one wins, and how do you guarantee a consistent result? - A customer cancels a confirmed order. Which of the fields you just fixed must change, and in what order?

Overview: A project-debugging task on a Django ticketing service: fix the order confirmation flow so held tickets become sold, seat counts stay correct, expired holds are rejected, Redis hold data is cleaned up and sold-out status is set. It tests transactions, row locking, idempotency, time zones and tests.

Goldman Sachs logo
Goldman Sachs
Oct 14, 2026
mediumSoftware EngineerOnline AssessmentSoftware Engineering Fundamentals
4
0

This task comes from the project-debugging section of an online assessment. You are given an existing Django ticketing project with documentation and a test suite. A customer first places a hold on tickets for an event, then confirms an order to buy them. The confirm-order flow does not behave as documented; fix it so that it does. The flow covers five concerns: updating ticket status, keeping the event's seat count right, rejecting expired holds, cleaning up the hold's data in Redis, and deciding when the event is sold out.

The project's code and documentation were not reported. The models, documented rules and current implementation below are an illustrative stand-in so the task can be practised; treat them as the codebase you were handed.

# models.py (illustrative)
from django.conf import settings
from django.db import models


class Event(models.Model):
    name = models.CharField(max_length=200)
    capacity = models.PositiveIntegerField()
    seats_sold = models.PositiveIntegerField(default=0)
    is_sold_out = models.BooleanField(default=False)


class Hold(models.Model):
    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    expires_at = models.DateTimeField()
    released = models.BooleanField(default=False)


class Ticket(models.Model):
    class Status(models.TextChoices):
        AVAILABLE = "available"
        HELD = "held"
        SOLD = "sold"

    event = models.ForeignKey(Event, on_delete=models.CASCADE)
    seat = models.CharField(max_length=20)
    status = models.CharField(max_length=10, choices=Status.choices, default=Status.AVAILABLE)
    hold = models.ForeignKey(Hold, null=True, blank=True, on_delete=models.SET_NULL)


class Order(models.Model):
    hold = models.OneToOneField(Hold, on_delete=models.PROTECT)
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
    created_at = models.DateTimeField(auto_now_add=True)

Documented behaviour (illustrative):

  1. Only the customer who placed a hold can confirm it, and only while it is unexpired and not released. A hold whose expires_at is at or before the server's current time has expired.
  2. Confirming changes every ticket in the hold from held to sold and creates exactly one order for the hold.
  3. Event.seats_sold increases by the number of tickets sold.
  4. Placing a hold writes a Redis key hold:{hold_id} (the hold's ticket ids, with a time-to-live) and adds the hold's ticket count to a counter event:{event_id}:held . Confirming deletes the hold key and subtracts the hold's ticket count from the counter.
  5. The event is sold out once seats_sold reaches capacity .
  6. Confirming a hold that already has an order returns that order and changes nothing.
# services.py (illustrative): the current implementation
from datetime import datetime

from .cache import redis_client
from .models import Hold, Order, Ticket


class HoldExpired(Exception):
    pass


def confirm_order(hold_id, user):
    hold = Hold.objects.get(pk=hold_id)
    if hold.expires_at < datetime.now():
        raise HoldExpired(hold_id)

    tickets = Ticket.objects.filter(hold=hold)
    for ticket in tickets:
        ticket.status = Ticket.Status.SOLD

    event = hold.event
    event.seats_sold += 1
    event.save()

    order = Order.objects.create(hold=hold, user=user)
    redis_client.delete(f"hold:{hold_id}")

    if event.seats_sold > event.capacity:
        event.is_sold_out = True
        event.save()
    return order

Find every place where confirm_order breaks the documented behaviour, fix it, and use the test suite, extended wherever a rule is untested, to show that each fix works.

Constraints and Clarifications

  • In the stand-in project, settings use USE_TZ = True , so expires_at values are timezone-aware.
  • Several customers can confirm holds for the same event at the same moment, and a client may retry a confirmation after a timeout.

Clarifying Questions Guidance

  • Is the database or Redis the source of truth for which seats are held, and what else reads the event:{event_id}:held counter?
  • When a confirmation arrives for an expired hold, should it release the hold's tickets immediately, or leave that to the job that expires holds?
  • Should a repeated confirmation return the existing order even if the hold has expired since the first one?
  • Which database runs in production and in the tests, and does it support row-level locks?

What a Strong Answer Covers Guidance

  • Maps every documented rule to the code and names each defect together with the failure it causes
  • A timezone-aware expiry check with the documented boundary, plus ownership and released-hold checks
  • Ticket status changes that are saved and limited to held tickets, and a seat count that adds the number of tickets sold without lost updates under concurrency
  • One atomic transaction with suitable locking, an idempotent retry path, and a correct sold-out condition
  • Redis cleanup that runs only after commit, tolerates Redis failures and keeps the held counter consistent
  • Tests that fail before each fix and pass after it, including boundary and retry cases

Follow-up Questions Guidance

  • Two requests confirm the same hold at the same moment. Walk through what your version does for each.
  • Redis is unavailable when an order is confirmed. What does the customer see, and how is the stale hold data cleaned up?
  • The job that expires holds runs while a confirmation of the same hold is in flight. Which one wins, and how do you guarantee a consistent result?
  • A customer cancels a confirmed order. Which of the fields you just fixed must change, and in what order?