SwadhyAI Notes

All Low-Level Design notes

Unit of work

Where transactions begin and end

A is the set of changes that must all succeed or all roll back. Example: park() marks a spot occupied, then saves a ticket. If the save fails, the spot is taken and nobody has a ticket. That pair is one unit. The transaction starts at the use-case edge — park() — not inside ParkingSpot.setOccupied.

Swimlane: the use case owns commit

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
  participant S as ParkingService
  participant L as ParkingLot
  participant R as TicketRepository
  participant U as UnitOfWork

  S->>U: begin
  S->>L: park(vehicle)
  L->>L: occupy spot
  S->>R: save(ticket)
  S->>U: commit
  Note over L: Spot.occupy does not commit
ParkingService.park()     ← transaction / unit of work starts
    allocate + occupy
    tickets.save
    commit

ParkingSpot.occupy()      ← no commit here

Begin at the use case

Controller or ParkingService. That is the story: park, exit, pay. there.

Entities change memory

Spot.tryOccupy() is not allowed to call connection.commit(). You could not compose occupy with save.

Fail together

if save fails. synchronized alone is mutual exclusion — compensate occupy, or use JDBC rollback.

Java: use case owns the unit of work

java
class ParkingService {
  private final ParkingLot lot;
  private final TicketRepository tickets;
  private final UnitOfWork uow;

  Ticket park(Vehicle vehicle) {
    return uow.inTransaction(() -> {
      Ticket ticket = lot.park(vehicle);
      tickets.save(ticket);
      return ticket;
    });
  }
}

interface UnitOfWork {
  <T> T inTransaction(Supplier<T> work);
}

In-memory rounds still have a unit of work

A synchronized park() is mutual exclusion: two gates cannot interleave occupy and save. It is not a database transaction — if tickets.save throws after spot.occupy(), you still need an explicit undo (or occupy only after save can succeed). Call that out: the lock stops races; rollback is a separate story. When you add JDBC, the same use-case method opens the real transaction. HLD depth is ACID and isolation levels. You do not need SERIALIZABLE on the whiteboard. You need to name what is .

Java: in-memory atomic park

java
class ParkingLot {
  private final Object gate = new Object();
  private final TicketRepository tickets;

  Ticket park(Vehicle vehicle) {
    synchronized (gate) {
      // Mutual exclusion only — not JDBC rollback. Undo occupy if save fails.
      ParkingSpot spot = allocation.choose(freeSpots(), vehicle)
          .orElseThrow(SpotUnavailable::new);
      spot.occupy();
      try {
        Ticket ticket = Ticket.open(ids.next(), vehicle, spot.id(), clock.instant());
        tickets.save(ticket);
        return ticket;
      } catch (RuntimeException e) {
        spot.free(); // explicit compensate; synchronized alone does not roll back
        throw e;
      }
    }
  }
}

Mistakes

  • Commit inside a setter — spot.setOccupied() that calls connection.commit() cannot share a transaction with tickets.save(). Commit at the use case.
  • Two writes with no story — occupy, then save, with no lock and no transaction, is how you get a taken spot and no ticket. Say which two steps are atomic before you code them.
  • JDBC commit on ParkingLot — that drags Connection onto the lot. Keep the unit of work next to the service, the Connection on the adapter. See persistence boundary.

Outbox is a later story

If park must also notify a display board, you will want the write and the event in one unit — the outbox pattern. In LLD, say “I would commit ticket plus outbox row together.” Do not start a second Connection in Ticket.close(). The GRASP controller is the object that runs the use case and therefore the natural commit point. Two gates racing for spot 101 is thread safety: the lock and the transaction are cousins.

Nested transactions are a smell here

If Spot.occupy opens a transaction and ParkingService.park opens another, you now have two commits or a savepoint maze. Keep one unit per use case. Nested “just in case” commits are how you persist occupy without the ticket. The GRASP controller / application service is the one object allowed to say commit. Repositories execute statements inside that unit; they do not start their own unless you are in a standalone script.

An untyped process(Object) API has no named unit of work: every map key might write. park(Vehicle) is a named unit. That is why the class API and the transaction boundary are the same conversation: name the use case, then say what must land together.

Java: adapter commits only when the service says so

java
class JdbcUnitOfWork implements UnitOfWork {
  private final DataSource dataSource;

  public <T> T inTransaction(Supplier<T> work) {
    Connection connection = null;
    boolean previousAutoCommit = true;
    try {
      connection = dataSource.getConnection();
      previousAutoCommit = connection.getAutoCommit();
      connection.setAutoCommit(false);
      T result = work.get();
      connection.commit();
      return result;
    } catch (RuntimeException | SQLException e) {
      if (connection != null) {
        try { connection.rollback(); } catch (SQLException ignored) {}
      }
      throw (e instanceof RuntimeException re) ? re : new PersistenceFailed(e);
    } finally {
      if (connection != null) {
        try {
          connection.setAutoCommit(previousAutoCommit);
          connection.close();
        } catch (SQLException ignored) {}
      }
    }
  }
}

Two gates, one spot

Concurrent park on spot 101 is a race plus a transaction. The lock (or DB unique constraint on occupied spot) makes the check-and-occupy atomic. The transaction makes occupy-and-save atomic. You need both stories. Saying “I will synchronized park” covers the in-memory interview. Saying “unique(spot_id) where open” covers JDBC. Neither lives inside ParkingSpot.setOccupied as a commit call.

Exercises

  1. Name the unit of work for exit. What rolls back if payment recording fails after the spot is freed?
  2. Why must ParkingSpot.occupy() not call commit, even if it has a Connection in a bad design?
  3. In a single-threaded toy lot, what still counts as a “transaction” between occupy and save?
RepositoryControllerACID (HLD)

Worked scene: occupy without a ticket

Gate A occupies spot 101 in memory. tickets.save throws. Without a unit of work, 101 stays occupied and the driver has no TicketId. The next park thinks the lot is tighter than it is. With a transaction or a synchronized park that only occupies after save can be attempted as one block, both map puts land or neither does. In JDBC, rollback restores the spot row. In memory, you undo occupy on failure or never expose occupy until save returns.

Explain that recovery plan out loud. Interviewers grade the story more than the UnitOfWork interface. An untyped process(Object) API has no story: you cannot name which writes share a commit. park(Vehicle) is the name of the unit.

java
Ticket park(Vehicle vehicle) {
  return uow.inTransaction(() -> {
    ParkingSpot spot = allocation.choose(freeSpots(), vehicle)
        .orElseThrow(SpotUnavailable::new);
    Ticket ticket = Ticket.open(ids.next(), vehicle, spot.id(), clock.instant());
    spot.occupy();
    tickets.save(ticket);
    spots.save(spot);
    return ticket;
  });
}

How to explain this in an interview

park unit

Occupy + ticket save. One lock in memory. One JDBC transaction in production.

exit unit

Free spot + close ticket + payment record. Name it before coding.

Who commits

ParkingService / use-case edge. Not Spot.occupy. Not Ticket.close.

Connection.commit

UnitOfWork adapter. Never a field on ParkingLot.

In about a minute, say: “park is occupy plus save. That pair is one unit. Spot.occupy does not commit. In memory I synchronized park; with JDBC the service starts the transaction. Two gates on spot 101 need a lock and a unit of work.”

If save fails after occupy with no rollback, the lot is inconsistent: a taken spot and no ticket. Name the unit before you code the two writes. An untyped process(Object) API has no named unit.

Java: exit as one unit

java
Receipt exit(TicketId id) {
  return uow.inTransaction(() -> {
    Ticket ticket = tickets.findById(id).orElseThrow(() -> new UnknownTicket(id));
    Receipt receipt = ticket.close(clock.instant(), pricing);
    ParkingSpot spot = spots.findById(ticket.spotId())
        .orElseThrow(() -> new InconsistentLot("ticket " + id + " has no spot"));
    spot.free();
    tickets.save(ticket);
    return receipt;
  });
}

Takeaway

Name the unit of work that must succeed or roll back. park commits allocation plus ticket write together. Entities do not commit. JDBC transactions start at the use-case edge, not inside a spot setter. Never put Connection.commit on ParkingLot.