Where transactions begin and end in code

Transactions, isolation and locking Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Order the steps

Order the steps of a 'place order' operation so no side effect escapes a rolled-back transaction.

  • Validate the request (no transaction yet)
  • COMMIT
  • Consumer sends the confirmation email
  • BEGIN; insert order and order lines; insert an outbox row
  • Relay publishes the outbox row to the broker

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A D B E C

  1. Validate the request (no transaction yet) Cheap checks before taking any locks.
  2. BEGIN; insert order and order lines; insert an outbox row All database writes, one atomic unit.
  3. COMMIT Only now is the order real.
  4. Relay publishes the outbox row to the broker Happens after commit, so no ghost events.
  5. Consumer sends the confirmation email Email follows the event, never runs inside the transaction.

30-second answer

One transaction per business operation: everything that must succeed or fail together, and nothing else. Open it as late as possible, close it as early as possible, and keep it in one place, usually the service layer, not scattered across repositories. Never do slow or irreversible things inside it: HTTP calls, sending email, publishing to a broker, or waiting for user input.

Practise saying it · 35 s. No recording, just a timer

Explain how you choose transaction boundaries.

Say your answer out loud, then check which of these points you covered:

  • One business operation
  • Short: open late, close early
  • Owned by the service layer
  • No HTTP, email or publish inside
  • Outbox for events

Deep dive

psycopg 3: with conn.transaction(): commits on success and rolls back on exception.

Reads that drive a decision belong inside the transaction, at a suitable isolation level.

Publishing events reliably from a transaction is what the transactional outbox pattern is for.

Autocommit mode is fine for single statements; it makes each statement its own transaction.

A scoped transaction with psycopg 3 Ran on Python 3.13.5 · mypy --strict
Python 3.13
import psycopg

with psycopg.connect(autocommit=True) as conn:
    conn.execute(
        "CREATE TEMP TABLE orders (id serial PRIMARY KEY, total int)"
    )
    conn.execute(
        "CREATE TEMP TABLE outbox"
        " (id serial PRIMARY KEY, topic text, payload jsonb)"
    )
    try:
        with conn.transaction():
            conn.execute("INSERT INTO orders (total) VALUES (100)")
            conn.execute(
                "INSERT INTO outbox (topic, payload)"
                " VALUES ('order.placed', '{\"id\": 1}')"
            )
            raise RuntimeError("payment check failed")
    except RuntimeError:
        pass
    row = conn.execute(
        "SELECT (SELECT count(*) FROM orders),"
        " (SELECT count(*) FROM outbox)"
    ).fetchone()
    print(row)

Common mistakes

  • Opening a transaction per HTTP request by default, middleware style, including slow external calls.
  • Committing in the middle of a repository method, so callers can't compose operations.
  • Publishing to Kafka before the commit, so consumers can see events for rolled-back data.

Senior take & follow-ups

Tie boundaries to business operations, keep them short, and push side effects to after commit.

Follow-up questions

Think of your answer first, then open the hint.

  1. How do you send an event only if the transaction commits?

    Show hint

    Write it to an outbox table in the same transaction; a relay publishes it.

  2. Who owns the transaction in a layered app?

    Show hint

    The use-case or service layer that knows the business operation.

Suggest a fix if something here is wrong or out of date.