SwadhyAI Notes

All Low-Level Design notes

Port plus adapter

Repository abstraction

A is a typed port for loading and saving one aggregate — usually save, findById, and maybe search. Example: tickets must come back at exit. A HashMap on ParkingLot works until someone asks “what if we persist?” With a repository, the lot calls the port and does not care whether the implementation is a map or a database.

One per aggregate

TicketRepository, not AppRepository.save(Object).

Swap the impl

HashMap today. JDBC tomorrow. Ticket unchanged.

Domain in, domain out

No Map<String,Object> rows leaking into park().

Collection-shaped, not table-shaped

Think of a repository as for one : you save a Ticket and look it up by id. Do not add table-shaped methods like updateFeeColumn. Add a only when a use case needs it. Pages and filters belong on search — see collection APIs. The boundary rule is persistence out of the domain: JDBC stays in the adapter.

Java: port plus in-memory adapter

java
interface TicketRepository {
  void save(Ticket ticket);
  Optional<Ticket> findById(TicketId id);
}

class InMemoryTicketRepository implements TicketRepository {
  private final Map<TicketId, Ticket> store = new HashMap<>();

  public void save(Ticket ticket) {
    store.put(ticket.id(), ticket);
  }

  public Optional<Ticket> findById(TicketId id) {
    return Optional.ofNullable(store.get(id));
  }
}

How to explain JDBC in an interview

Explain this clearly: “JdbcTicketRepository would implement the same interface. Connection lives there.” That sentence is the design. Inject the repository. Tests use the in-memory map. Wiring is DI and DIP. Choosing SQL vs a document store is SQL vs NoSQL— pick later; keep the port now.

Java: a query the use case needs

java
interface TicketRepository {
  void save(Ticket ticket);
  Optional<Ticket> findById(TicketId id);
  Optional<Ticket> findOpenBySpot(SpotId spotId);
  Page<Ticket> search(TicketQuery query);
}

class ParkingLot {
  private final TicketRepository tickets;

  Receipt exit(TicketId id) {
    Ticket ticket = tickets.findById(id).orElseThrow(() -> new UnknownTicket(id));
    Receipt receipt = ticket.close(clock.instant(), pricing);
    tickets.save(ticket);
    return receipt;
  }
}

One God repository for the whole app

AppRepository.save(Object) is the same mistake as an untyped process(Object) API — one blob method for all storage. One repository per aggregate: tickets, maybe spots if they are stored separately. A facade that delegates to several repositories is a different pattern — and still does not take Object.

Repository that returns HashMap rows

findById returning Map<String,Object> leaks the table into the lot. Return Ticket. Map inside the adapter. Rebuild Money and status there — that is the persistence model mapping.

DAO vs repository in an interview

A DAO often mirrors a table. A repository mirrors an aggregate. If you only have one table and one entity, they look similar. Still return Ticket, not ResultSet. The name you want on the whiteboard is repository.

Identity and save

save is upsert in most LLD rounds: put by TicketId. Do not invent a separate insert and update unless the interviewer asks about unique constraints. The in-memory map is honest: one id, one ticket. If two threads save, that is a concurrency story — thread safety— not a reason to put synchronized on JDBC inside ParkingLot.

What the lot is allowed to know

ParkingLot may know tickets.save and tickets.findById. It may not know SELECT, ResultSet, or table names. If you need “open tickets on floor 2,” add findOpenByFloor to the port and implement it twice: stream-filter in memory, WHERE in JDBC. Do not pass a SQL string from the lot into the repository — that is the lot writing SQL with extra steps.

An untyped AppRepository.save(Object) is the storage version of process(Object): one bag method that hides every aggregate. You lose the aggregate, the type, and the ability to swap impls without rewriting callers. TicketRepository is the persistence equivalent of park(Vehicle): named, typed, and small.

Java: JDBC adapter still returns Ticket

java
class JdbcTicketRepository implements TicketRepository {
  private final Connection connection;

  public Optional<Ticket> findById(TicketId id) {
    try (PreparedStatement ps = connection.prepareStatement(
        "SELECT id, plate, status, entry_ms FROM ticket WHERE id = ?")) {
      ps.setString(1, id.value());
      ResultSet rs = ps.executeQuery();
      if (!rs.next()) return Optional.empty();
      return Optional.of(mapRow(rs));
    } catch (SQLException e) {
      throw new PersistenceFailed(e);
    }
  }

  private Ticket mapRow(ResultSet rs) throws SQLException {
    return Ticket.rehydrate(/* columns → value objects */);
  }
}

How to explain this in an interview

“I will inject TicketRepository. InMemory for the interview. JDBC would implement the same interface; Connection stays in that class. ParkingLot.park still takes Vehicle.” That explanation plus a HashMap implementation is a complete persistence design for machine coding.

Exercises

  1. Write TicketRepository with save and findById. Implement InMemory. Where would Connection appear if you added JDBC?
  2. A teammate adds updateFeePaise(long) to the interface. Why is that table-shaped, and what domain method replaces it?
  3. Can you swap the HashMap for a database without rewriting Ticket? What must stay true of findById’s return type?
Persistence boundarySearch and pagesDIP

Worked scene: exit without SQL in the lot

The driver presents TicketId T-9. ParkingLot.exit asks tickets.findById. The in-memory map returns the Ticket. Ticket.close prices the stay. tickets.save writes it back. Nowhere in that exit story is a JDBC Connection. If tomorrow findById hits Postgres, that same exit story still works — that is the swap test. If park() had to declare throws SQLException, the design failed: the lot would depend on SQL.

Keep query methods honest: findOpenBySpot returns Optional<Ticket>, not a row of column names. Search returns Page<Ticket>. The adapter may run JOIN; the lot still sees Tickets. If you feel the urge to add execute(String sql) to the interface, you are writing process(Object) for storage. Stop and name the use case instead.

java
interface SpotRepository {
  Optional<ParkingSpot> findById(SpotId id);
  void save(ParkingSpot spot);
}

class ParkingLot {
  private final TicketRepository tickets;
  private final SpotRepository spots;
  // two ports, two aggregates — still no JDBC here
}

How to explain this in an interview

Interface size

save, findById, then the one query the use case needs. Not every WHERE.

Return type

Ticket or Page<Ticket>. Never Map<String,Object> rows.

How many repositories

One per aggregate. Tickets, maybe spots. Never AppRepository.save(Object).

JDBC

Same interface. Connection in the adapter. mapRow rebuilds Ticket.

In about a minute, say: “TicketRepository.save and findById. InMemory is a HashMap. JDBC implements the same port. I return Ticket, not rows. One repository per aggregate — not AppRepository.save(Object).”

updateFeePaise on the interface is table-shaped. ticket.close plus save is domain-shaped. Search lives as search(TicketQuery), not execute(sql) from the lot.

The swap test is the design: change InMemory for Jdbc, keep park(Vehicle) and Ticket. If findById returns a row map, you failed the test.

Two aggregates means two ports — tickets and spots — not one God repository that takes Object.

That explanation plus a HashMap implementation is enough for the interview.

Java: findOpen stays a query on the port

java
interface TicketRepository {
  void save(Ticket ticket);
  Optional<Ticket> findById(TicketId id);
  List<Ticket> findOpen();
}

class InMemoryTicketRepository implements TicketRepository {
  public List<Ticket> findOpen() {
    return store.values().stream()
        .filter(Ticket::isOpen)
        .toList(); // copy via stream; still not the live map
  }
}

Takeaway

Give aggregates a repository: save, findById, the queries you need. Hide SQL, files, or maps. Return Ticket, not rows. One repository per aggregate — never an untyped save(Object) for all aggregates. Swap HashMap for a database without rewriting Ticket.