Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Low-Level Design (Object-Oriented Design)  Module 5 – Modelling lifecycles, time, money and rules

Designing lifecycles as state machines

Find a lifecycle's states and events from its requirements, timeouts and failures included, then record each transition and make retries safe.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7 and Node.js 24.21.0
  • By MySmartCoPilot
Free core, Pro depth: 11 of 30 minutes free
Free: Four questions that find the events · A seat hold, run against a clock it is given · What a complete lifecycle design adds. With a pass: 2 in-depth parts and 3 interview answers, about 19 minutes.

What you will learn

  • Discover states and events from requirements, including timeouts and failures
  • Record every transition with its time, actor and reason for auditing
  • Design idempotent transitions so that a resent event changes nothing
  • Undo a failed step with explicit compensating transitions

Before you start

What you’ll learn · Before you start

What you will learn

  • Discover states and events from requirements, including timeouts and failures
  • Record every transition with its time, actor and reason for auditing
  • Design idempotent transitions so that a resent event changes nothing
  • Undo a failed step with explicit compensating transitions

Before you start

On this page

Designing a lifecycle as a state machine means deciding, before writing code, every state an object can be in, every event that moves it, who sends each event, and what the object does when an event comes twice or never comes at all. Those last two cases are where many lifecycle bugs come from: a payment callback delivered twice, a seat hold that nobody confirms, a timer that fires after the customer has already paid. A good design answers each one with an explicit transition, writes every move into a history, and treats a resent request as a question already answered. This lesson finds the states and events of a cinema seat hold from its requirements, then runs the design against a clock that the code is given, so a test moves time instead of waiting for it.

Four questions that find the events

Requirements usually describe the happy path: a customer picks up to six seats, which are held for 8 minutes while they pay, and a successful payment confirms them. To find the rest of the lifecycle, ask four questions of every state. For a held seat, the answers are the design:

  1. What can happen next? List every event the state might receive, wanted or not. Here the payment succeeds or fails, the customer cancels, or the time runs out, so four events leave held.
  2. Who sends it? A person, another system or the clock. Here the payment provider, the customer and the clock all send events, and those from systems and the clock arrive at awkward moments.
  3. What if it happens twice? Networks resend, people double-click and payment providers retry their callbacks. Here a resent success must not confirm the seats or charge the customer again.
  4. What if it never happens? Then something else must end the state, usually a deadline. Here the customer walks away, so after 8 minutes the hold expires and the seats go back on sale.

The fourth question is the easiest to miss, because nobody types that event: the deadline belongs to the hold itself, and the clock delivers it.

State diagram of a seat hold: from held to confirmed, released or expired, each arrow naming who sends the event.customer picks seatsheldexpires 8 min after it startsconfirmed(final)released(final)expired(final)for every arrow, also ask:what if it arrives twice?what if it never arrives?hold(seats)payment succeededfrom the payment providercancel or payment failedfrom the customer or the providerdeadline passedfrom the clock

A seat hold, with the sender of every event

Text description of the diagram

A hold starts when the customer picks seats, in the state "held", which expires 8 minutes after it starts. Three arrows leave it, each to a final state:

  • "Payment succeeded", sent by the payment provider, leads to confirmed.
  • "Cancel or payment failed", sent by the customer or by the provider, leads to released, and the seats go back on sale.
  • "Deadline passed", sent by the clock, leads to expired.

A note beside the diagram asks two more questions of every arrow: what happens if the event arrives twice, and what happens if it never arrives. Confirmed, released and expired accept no further events.

A seat hold, run against a clock it is given

The hold below never reads the computer’s clock. It receives a clock object with a now() method, so the script can move time to any second it likes. The hold stores its deadline as an absolute time, and every event checks the deadline before anything else, because the scheduler that should expire the hold may run late or not at all.

Five runs of one seat hold, on a clock the script moves Python · seat_hold/seat_hold.py
"""A cinema seat hold: held for 8 minutes while the customer pays, then confirmed, released or
expired. The hold reads the time only from the clock it is given, so this script moves a fake
clock instead of waiting."""

HOLD_SECONDS = 8 * 60


class FakeClock:
    """A clock that the caller moves by hand. now() counts seconds from the start of the run."""

    def __init__(self):
        self.seconds = 0

    def now(self):
        return self.seconds

    def move_to(self, minutes, seconds=0):
        self.seconds = minutes * 60 + seconds


class Refused(Exception):
    """An event that the hold's current state does not accept."""


class SeatHold:
    def __init__(self, seats, clock):
        self.seats = seats
        self.clock = clock
        self.state = "held"
        self.expires_at = clock.now() + HOLD_SECONDS
        self.trail = [(clock.now(), "held", "customer")]

    def _move(self, to, actor):
        self.state = to
        self.trail.append((self.clock.now(), to, actor))

    def tick(self):
        """The clock's event: a scheduler calls it, and a hold past its deadline expires."""
        if self.state == "held" and self.clock.now() >= self.expires_at:
            self._move("expired", "clock")

    def confirm(self, payment_id):
        """The payment provider's event: the customer has paid."""
        self.tick()  # no event may act on a hold that should already have expired
        if self.state != "held":
            raise Refused(f"confirm({payment_id}) refused: the hold is {self.state}")
        self._move("confirmed", f"payment {payment_id}")

    def release(self, actor):
        """The customer's event, or a failed payment's: the seats go back on sale."""
        self.tick()
        if self.state != "held":
            raise Refused(f"release refused: the hold is {self.state}")
        self._move("released", actor)


def clock_text(seconds):
    return f"{seconds // 60}:{seconds % 60:02d}"


def show(title, hold, notes=()):
    print(title)
    for at, state, actor in hold.trail:
        print(f"  {clock_text(at)}  {state:<10} by {actor}")
    for note in notes:
        print(f"  {note}")


def run(title, steps):
    """steps: (minutes, seconds, action) in time order, with action one of the hold's events."""
    clock = FakeClock()
    hold = SeatHold(["F7", "F8"], clock)
    notes = []
    for minutes, seconds, action in steps:
        clock.move_to(minutes, seconds)
        try:
            action(hold)
        except Refused as refusal:
            notes.append(f"{clock_text(clock.now())}  {refusal}")
    show(title, hold, notes)


run("1. Paid with a second to spare", [(7, 59, lambda h: h.confirm("P-1"))])
run("2. Nobody pays: the scheduler ticks at 7:59 and at 8:00",
    [(7, 59, SeatHold.tick), (8, 0, SeatHold.tick)])
run("3. The customer gives up", [(2, 30, lambda h: h.release("customer"))])
run("4. The provider sends the same success twice",
    [(3, 10, lambda h: h.confirm("P-4")), (3, 50, lambda h: h.confirm("P-4"))])
run("5. A success that arrives after the deadline", [(8, 5, lambda h: h.confirm("P-5"))])

Output

1. Paid with a second to spare
  0:00  held       by customer
  7:59  confirmed  by payment P-1
2. Nobody pays: the scheduler ticks at 7:59 and at 8:00
  0:00  held       by customer
  8:00  expired    by clock
3. The customer gives up
  0:00  held       by customer
  2:30  released   by customer
4. The provider sends the same success twice
  0:00  held       by customer
  3:10  confirmed  by payment P-4
  3:50  confirm(P-4) refused: the hold is confirmed
5. A success that arrives after the deadline
  0:00  held       by customer
  8:05  expired    by clock
  8:05  confirm(P-5) refused: the hold is expired

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 seat_hold.py

Runs 1 to 3 are the expected paths, and run 2 checks the boundary: at 7:59 the hold is still held, and at exactly 8:00 it expires. The last two runs show what the four questions predicted. In run 4 the provider resent a success that had already confirmed the seats, and the design answered with an error, so the provider is told that a payment it completed has failed. In run 5 no scheduler tick came at 8:00, so the late success itself found the hold past its deadline, expired it and was refused, and the customer has paid for seats they do not have. Both are refusals of real events, and both need more design, not more if statements.

What a complete lifecycle design adds

Five techniques turn this sketch into a design that holds up in production, and each one answers a failure above:

  • An append-only transition history records every move with its time, its actor and a reason; the current state is the last entry.
  • Idempotent transitions answer a resent request id with the result it got the first time, instead of an error or a second effect.
  • Conflict detection refuses a known request id that arrives with a different event, because that is a client bug, not a retry.
  • Timeout policies decide, as a business rule, what happens to an event that arrives just after a deadline.
  • Compensating transitions undo a reservation on purpose, with a reason, when a later step fails.

Check yourself

3 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 3 Which of the four questions turns up the expiry of a seat hold?

    Choose one answer.

    Show the answer to question 1

    Answer: What if the event never happens?

    Asking what happens when the customer never pays is what reveals that something else must end the hold. The answer is a time event, sent by the clock, with its own arrow from held to expired.

  2. Question 2 of 3 In the seat hold, which events are sent by something other than the customer?

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • The expiry of the hold
    • Payment succeeded

    The clock sends the expiry and the payment provider sends the result of the payment. Designs that list only what a user does miss both, and both arrive at awkward moments: late, early or twice.

  3. Question 3 of 3 In the fifth run of seat_hold.py, no scheduler tick came at 8:00 and the payment success arrived at 8:05. Why did the hold end up expired rather than confirmed?

    Choose one answer.

    Show the answer to question 3

    Answer: Every event first checks the deadline, so the late event expired the hold and was then refused

    confirm() calls tick() before it looks at the state, so a hold past its deadline expires at the first event that sees it, scheduler or not. The success is then refused because the hold is expired, and the money has to go back to the customer.

Recording every transition, and retries that change nothing Pro: opens with a Pro, Premium or Ultimate pass

A lifecycle that keeps only its current state cannot say how it got there, and it cannot tell a resent request from a new one. Both are fixed by one small change: every accepted event becomes an entry in a history, filed under its request id.

In this part:

  • Writing the history, and replaying it to check the invariant
  • Duplicates measured: three designs, 10,000 deliveries

2 runnable examples (Python) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Read a complete sample first: Low-level design: parking lot (the model)

Timeouts, late events and compensation, tested without sleeping Pro: opens with a Pro, Premium or Ultimate pass

The clock and failures send the events that nobody types, and both tend to arrive at the worst moment. This part turns a deadline into a policy the business chooses, measures what each choice costs, and undoes a half-finished booking with transitions instead of deletions.

In this part:

  • A success that arrives after the deadline: two policies, measured
  • Compensation: a failure after a reservation, drilled at every step
  • Practise: a courier shipment with three attempts and duplicate scans
  • A courier shipment with three delivery attempts and duplicate scans
    • Results of the sample tests

2 runnable examples (Python) · 1 exercise with sample tests · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher · Warm-up

    Which questions should you ask of every state when you design a lifecycle, and which one do designs most often miss?

    Four questions, asked of each state: what can happen next, who sends each event, what happens if an event arrives twice, and what happens if it never arrives. The first lists the events, wanted or not. The second shows that people are not the only senders: other systems and the clock send events too, often at awkward moments. The third covers resends from networks, double clicks and payment callbacks that providers retry. The fourth is the one most designs miss, because nobody types that event: if a customer never pays, something else must end the hold, so the design needs a deadline stored with the object and a time event, sent by the clock, that moves it to a final state such as expired. Answer all four for every state and the diagram has most of its arrows.

  2. Mid to senior

    A payment provider delivered its success callback for one booking twice, 40 seconds apart. Customers received two ticket emails, and the logs show an error for the second callback. Redesign the booking's lifecycle so that duplicates are harmless, and say how you would test it.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Seat holds expire after 8 minutes. A customer pays at 7 minutes 58 seconds, but the provider's callback reaches you at 8 minutes 3 seconds, after the hold has expired. What should happen, and how does your state machine make that outcome a decision rather than an accident?

    Model answer, Pro. First step and follow-ups

  4. Fresher to mid

    Support must answer "who cancelled this order, when and why?" for any order of the last two years. Design how the order's lifecycle records its transitions, and explain how the current status and the history stay consistent.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Ask four questions of every state: what can happen next, who sends it, what if it happens twice, and what if it never happens.
  • The clock is an event source. Store each deadline with its object and check it on every event, so that tests move time instead of waiting.
  • An event that arrives twice or late is an ordinary input, and the design, not chance, decides what it does.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.