SOLID · I
Interface Segregation
Interface Segregation means clients should not be forced to depend on methods they do not use. Prefer several small, focused interfaces over one oversized interface that mixes unrelated operations.
For example, someone published ParkingOps with park, unpark, pay, refund, allocate, persist, exportCsv, and printDailyReport. The reporting screen then had to implement park() as a no-op. Ask whether this caller needs the whole interface or only one role. Split by role, not by noun. Occupiable is a role. Pricable is a role. Persistable is a role. ParkingLot may implement several roles. A CSV exporter implements none of the parking verbs.
A fat interface forces unused methods
In plain words: do not force a class to implement methods it does not use. A fat interface is a type with many unrelated methods glued together. Callers depend on park, pay, or persist — not on all three at once. Every extra method is another reason a change ripples, and another empty body waiting to lie.
Small interfaces also protect Liskov Substitution. When occupy() is not on the report port, nobody has to throw UnsupportedOperationException to “implement” it. ISP removes the method. LSP then only has to worry about the methods that remain.
In machine coding you rarely need ten interfaces. You need the two or three seams the follow-up will press: allocation, pricing, and persistence. That is enough segregation. One interface per class that duplicates every method — IParkingLot plus ParkingLot — is not segregation. It is a duplicate name.
ParkingOps as published
Everything a lot might ever do, in one type
The reporting job only needs a list of closed tickets. After this interface ships, the report class still has to pretend it can open a gate. Each unused verb is a lie the compiler cannot see if you stub it empty, or a landmine if you throw.
park(Vehicle)unpark(TicketId)pay(Ticket)refund(PaymentId)allocate(Vehicle)persist(Ticket)exportCsv()printDailyReport()openGate()closeGate()sendSms(Ticket)applyCoupon(Code)Java: roles instead of a fat interface
// Fat — every implementer pretends to do everything
interface ParkingOps {
Spot park(Vehicle v);
void unpark(TicketId id);
Money pay(Ticket t);
void refund(PaymentId id);
void save(Ticket t);
String csvReport();
void openGate();
}
// DailyCsv has no honest implementation of park() or openGate().Split until each caller depends on one role. ParkingLot can still implement two roles if it honestly does both jobs. DailyCsv implements only LotReport. A payment adapter implements only Checkout. Nobody writes void park() {}.
interface SpotAllocator {
Optional<ParkingSpot> park(Vehicle v);
}
interface Checkout {
Money pay(Ticket t, Clock clock);
}
interface TicketStore {
void save(Ticket t);
Optional<Ticket> find(TicketId id);
}
interface LotReport {
String csv(LocalDate day);
}
class ParkingLot implements SpotAllocator {
private final SpotAllocationStrategy allocation;
public Optional<ParkingSpot> park(Vehicle v) {
return allocation.choose(freeSpots(), v);
}
}
class ExitService implements Checkout {
private final PricingStrategy pricing;
public Money pay(Ticket t, Clock clock) {
return pricing.fee(t, clock);
}
}
class DailyCsv implements LotReport {
private final TicketStore tickets;
public String csv(LocalDate day) {
return tickets.closedOn(day).stream()
.map(Ticket::csvRow)
.collect(Collectors.joining("\n"));
}
}Split by role, not by noun
“ParkingLot” is a noun. Roles are verbs a caller needs: allocate, check out, store, and report. Two callers that need different verbs should not share one type. Two callers that need the same verb can share a small interface even if the implementers are different classes.
Implementers may wear several hats
Segregation does not mean one class, one interface. A rich ParkingLot might be a SpotAllocator and still not be a LotReport. The point is that the report code never compiles against park(). Changing park() does not force the report to recompile.
How ISP and DIP meet at the edge
Dependency Inversion says the domain owns the abstraction. ISP says that abstraction should be a small, focused interface rather than a fat one that mixes unrelated jobs. TicketRepository with save and find is focused. ParkingOps that also opens gates is oversized. JDBC adapters implement the focused interface. They should not have to stub openGate().
Payment is the same story. Checkout needs charge and maybe refund. It does not need CSV. A Stripe adapter that is forced to implement exportCsv will either throw or return garbage. Segregate first, then invert.
interface PaymentGateway {
PaymentId charge(Money amount, TicketId ticket);
}
interface Refunds {
void refund(PaymentId id, Money amount);
}
// A read-only ledger for finance reports does not implement charge().
interface PaymentLedger {
List<Payment> onDay(LocalDate day);
}When not to split
If every caller uses every method, one interface is honest. TicketRepository with save and find is already small. Splitting it into TicketWriter and TicketReader is only worth it when a caller must not write — a report, a read replica adapter, or a UI that only displays. Until that caller exists, two methods on one type is fine.
IParkingLot that mirrors ParkingLot one-for-one is a Java-bean habit, not ISP. You introduced a name without introducing a seam. Wait until two callers need different slices. Then cut the role they actually use.
Empty methods are the tell
void park() {} on a report class is a lie the compiler cannot see. So is throwing UnsupportedOperationException to “satisfy” the interface. Remove the method from that type instead. If you find three empty methods on a class, you found a fat interface and a Liskov failure in the same file.
In the parking-lot round, say: “The CSV job should not see park(). I will give it LotReport. ParkingLot will not implement LotReport; a small DailyCsv will.” That sentence is Interface Segregation without reciting the letters.
Compile coupling is the hidden cost
When DailyCsv depends on ParkingOps, a change to park() recompiles the report. Segregated LotReport does not mention park, so it does not recompile. That is Interface Segregation as a build-time habit, not only as a politeness to implementers. Fat interfaces make every client a friend of every verb.
class DailyCsv implements LotReport {
private final TicketStore tickets;
public String csv(LocalDate day) {
return tickets.closedOn(day).stream()
.map(t -> t.id() + "," + t.closedAt())
.collect(Collectors.joining("\n"));
}
}
class GatePanel {
private final SpotAllocator allocator;
void onCarArrived(Vehicle v) {
allocator.park(v).orElseThrow(() -> new IllegalStateException("full"));
}
}
// GatePanel does not recompile when LotReport grows a pdf() method.Role interfaces in a two-hour sketch
You will not draw twelve interfaces. Draw the ones with two callers or a likely follow-up: TicketRepository (service + report), PricingStrategy (exit + preview), and SpotAllocationStrategy (lot + tests). Skip IParkingLot. Skip splitting save and find until a read-only caller exists. That is ISP with YAGNI holding the other wrist.
If an implementer has three empty methods, stop coding and cut the interface. Do not comment “not supported.” The comment is a confession that ISP already failed and LSP is next.
Related
Preview fee is a second caller, not a second fat interface
A kiosk that shows “you would pay X if you left now” needs Checkout or PricingStrategy, not park() and not openGate(). That preview is why Checkout exists as a role before you have a second payment vendor. The kiosk class depends on fee(ticket, clock). It does not implement ParkingOps. Segregation earned its keep with two callers, not with a premature interface.
class FeePreview {
private final PricingStrategy pricing;
Money show(Ticket ticket, Clock clock) {
return pricing.fee(ticket, clock);
}
}IParkingLot is not a role
Prefixing I on the class name duplicates every method and helps no caller. A role is a slice a caller needs. SpotAllocator is a slice. LotReport is a slice. IParkingLot is a shadow. Delete the shadow; keep the slices that have two clients.
Two clients justify the split
ExitService and FeePreview both need fee(). DailyCsv needs closed tickets, not park(). GatePanel needs allocate, not csv(). Once you can name two clients with different slices, cut the role. Until then, TicketRepository with save and find is already segregated enough. ISP is a response to conflicting clients, not a quota of interfaces.
Three empty methods means stop
If DailyCsv has empty park, openGate, and refund, the fat interface has already won. Cut those methods off LotReport before you write another line of CSV. Segregation is cheaper than a page of unsupported comments.
Takeaway
Callers should depend only on the role they use. Split fat interfaces into Occupiable, Checkout, TicketStore, and LotReport. Do not invent IParkingLot as a twin of the class. Empty methods and surprise throws mean the interface was too wide — cut the method off that type instead of lying in the body.