SwadhyAI Notes

All Low-Level Design notes

Production LLD

Package and class organization

You open park() in a parking-lot machine-coding round. The call hops Main to util to helper to manager before it touches jdbc (Java's database API). Ticket and a StripeClient sit in one package named util. Another package named parking holds forty classes. That is a junk drawer, and the util mix is a fire hazard.

Code organization means packages and folders that show dependency direction: domain inward, adapters outward. The folder tree on the right should make the obvious.

A tree that shows the arrows

parkinglot/
  domain/          ParkingLot, Ticket, ParkingSpot, Money
  app/             ParkingService
  ports/           TicketRepository, Clock, PricingStrategy
adapters/
  persistence/     InMemoryTicketRepository
  time/            SystemClockAdapter   // wraps Clock.systemUTC()
  notify/          RecordingChannel, SmsChannel
Main.java          // wires adapters into ParkingService

What actually happens when the folders lie

The failure above is quieter than a production outage. The folders look tidy while the imports ignore them. If park() jumps through util, helper, and common, the organization is lying: those names are not homes for business rules.

A layout (parkinglot/, tickets/, pricing/) puts one capability nearby so you can follow the use case without opening six unrelated drawers. A layout (domain/, app/, adapters/) makes import direction obvious when Stripe, mail, and Postgres adapters grow. Mixing both without a rule produces random folders. The is the point, not the fashion.

How to choose a package layout

Once you see that folders can lie, you still have to pick a layout you can defend in a short interview round.

parkinglot/, tickets/, pricing/. Everything for parking in one place. Best default in a forty-five minute round.

  • Follow park() in one tree so the use case stays readable.
  • Feature teams own a folder, which keeps tickets and pricing nearby.
  • Risk: a fat parkinglot/ folder becomes another God package.

domain/, app/, adapters/. Clear import direction when adapters grow.

  • Adapters may import SDKs so vendor types stay off the domain.
  • Domain stays vendor-free, so tests construct types without a vendor SDK.
  • Risk: you open six directories just to follow one use case.

Simple rule of thumb

Pick one layout and enforce the . Feature packages are the default in a short round. Lean on layers when adapters grow. Do not mix both without a written rule.

Who may import whom

The layout only works if import arrows match the folders. Here is who may depend on whom, and which imports the domain is allowed to write.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
flowchart LR
  UI[UI / Main]
  App[ParkingService]
  Dom[Domain types]
  Ports[Ports]
  Ad[Adapters]
  UI --> App
  App --> Dom
  App --> Ports
  Ad --> Ports
  Ad --> SDK[Stripe JDBC Clock.systemUTC]

Arrows point inward to ports (interfaces the domain owns) and domain types. SDKs (vendor client libraries) never point at Ticket.

Clock is a port, not util. PricingStrategy is not helper. If you cannot name the package after a domain idea, the class may not belong. The in code looks like this: own ports are fine; SQL, Stripe, and LocalDateTime.now() inside an entity are not.

java
package parkinglot.domain;

import parkinglot.ports.Clock;           // allowed: you own it
import parkinglot.ports.TicketRepository; // allowed
// import java.sql.Connection;            // forbidden
// import com.stripe.StripeClient;        // forbidden
// import java.time.LocalDateTime; then .now() in the entity: forbidden

class Ticket {
  private final Instant enteredAt; // Instant is a value, not a hidden Clock
}

Mapper lives in the adapter

Import arrows are half of the line. The other half is not letting one class play three roles at the three edges. Do not put JPA (Java Persistence API annotations) on Ticket to "save a class." Map in the adapter. Tests construct Ticket directly. Production loads a record and maps. That keeps primitive obsession (nullable doubles in a row) out of the domain constructor.

java
class PostgresTicketRepository implements TicketRepository {
  public Optional<Ticket> find(TicketId id) {
    TicketRecord row = jdbc.query(id);
    if (row == null) return Optional.empty();
    return Optional.of(new Ticket(row.id(), row.plate(), row.enteredAt()));
  }
}

Three names, three reasons to change. Reusing one class for the database, the JSON wire, and the domain is a boundary leak, not a DRY (don't-repeat-yourself) win.

Ticket

domain

Invariants, identity, lifecycle. No JPA. No JSON annotations required.

TicketRecord / TicketEntity

adapters/persistence

Columns, lazy loads, schema. Mapper to Ticket.

TicketDto

API edge

Wire shape. Pagination fields. Never reused as the entity.

A walk through park()

The tree and the mapper are easier to trust when you walk one use case. A healthy walk is Main wires adapters, ParkingService coordinates, domain types change state, and ports hide Clock and TicketRepository.

  1. 1. Main.java

    new ParkingService(Clock.systemUTC(), sequentialIds(), nearest(), hourly(), postgres, sms)

  2. 2. app/ParkingService.java

    park(): allocate, Ticket.open(..., clock.instant()), tickets.save

  3. 3. domain/Ticket.java

    Invariants only. Instant stored, Clock not imported.

  4. 4. ports/TicketRepository.java

    save / find. No SQL types.

  5. 5. adapters/persistence/PostgresTicketRepository.java

    JDBC. Maps to Ticket (see mapper above). Tests never import this.

What still breaks after a clean tree

Putting the pieces together, a tidy folder tree still fails if types leak, catch-all names hide rules, or you cheat the folders in a short interview round.

Encapsulation is part of the tree

A perfect folder tree with Ticket.spot public still leaks. Organization is packages plus encapsulation, not directories alone. Primitive obsession (a public double fee) and feature envy (getters chained across packages) ignore the folders. Public fields are a package-level God object: every importer becomes a reason to change Ticket. Keep fields private, expose commands, and let adapters map.

Names that hide business rules

util/

Not a home for fees, tickets, or Clock

helper/

Not a home for fees, tickets, or Clock

manager/

Not a home for fees, tickets, or Clock

common/

Not a home for fees, tickets, or Clock

A Clock port is not util. A PricingStrategy is not helper. ParkingLotManager is often a God object waiting for a better name: ParkingService as a controller.

Common mistakes

domain importing jdbc

Then every test needs a database or a mocking festival. The folder said "domain," the import said otherwise. Move the SQL to an adapter (see Who may import whom).

Package by layer, then cheat with public fields

Same leak as encapsulation above: folders cannot save a public Ticket.spot.

One Ticket class for DB, JSON, and domain

That is the boundary leak in the naming section above, marketed as DRY.

Forty-five minutes is still a hexagon

You will not build a perfect package tree. You can still put interfaces next to the lot and HashMap fakes in the test source. That is organization (a hexagon: domain in the middle, adapters on the edge). A single file is a demo, not a package strategy. Tests sit next to the code or under src/test with the same packages. Pick one and keep it.

Wrap-up

You have the layout choice, the import rule, and the three-name split. Check that the ideas stuck, then follow the neighboring notes.

Check yourself

  1. Draw arrows from Main, ParkingService, Ticket, and PostgresTicketRepository. Which arrow is illegal?
  2. Where does Clock.systemUTC() live in the tree, and why not inside domain?
  3. Rename util/FeeMath.java. What package and type names would you use instead?

Takeaway

Make the folder tree match who may import whom. Domain stays free of JDBC, Stripe, and the wall clock. Ticket, TicketRecord, and TicketDto are three types because they change for three reasons.