SwadhyAI Notes

All Low-Level Design notes

Extra pattern · not GoF

Registry pattern

A is a map from a name (or key) to a strategy, factory, or handler. Lookups replace growing switches so a new type is one map entry, not three missed branches.

Example: car, bike, EV, van. A switch on vehicle type lives in the factory, in pricing, and in the serializer. A new type is three edits and one missed branch. A registry centralizes “given this key, which object?”

Who is allowed to know every implementation?

Factory creates

Factory Method hides new. The registry can own the map the factory uses.

Strategy does the work

Strategy is the algorithm. The registry chooses which strategy.

Registry chooses

Key in, collaborator out. One table per variation point. Fail the if the key is unknown.

Look up instead of switch

java
class PricingRegistry {
  private final Map<VehicleType, PricingStrategy> byType = new HashMap<>();

  void register(VehicleType type, PricingStrategy strategy) {
    byType.put(type, strategy);
  }

  PricingStrategy forType(VehicleType type) {
    var strategy = byType.get(type);
    if (strategy == null) throw new IllegalArgumentException(type.name());
    return strategy;
  }
}

class ParkingLot {
  private final PricingRegistry pricing;
  ParkingLot(PricingRegistry pricing) { this.pricing = pricing; }

  Money fee(Ticket ticket, Instant exit) {
    return pricing.forType(ticket.vehicle().type()).fee(ticket, exit);
  }
}

Register once, at the edge

java
// composition root — production
PricingRegistry pricing = new PricingRegistry();
pricing.register(VehicleType.CAR, new HourlyPricing());
pricing.register(VehicleType.BIKE, new BikePricing());
pricing.register(VehicleType.EV, new EvPricing());
ParkingLot lot = new ParkingLot(clock, tickets, pricing);

// tests register a fake
PricingRegistry testPricing = new PricingRegistry();
testPricing.register(VehicleType.CAR, (t, e) -> Money.of(100, "INR"));

That wiring is dependency injection. The registry is an ordinary object. Do not hide it behind getInstance(). See Singleton for why a global table makes tests share state.

One table per variation point

A single God-map that stores factories, prices, and JSON writers is a switch with extra steps. Split by the thing that varies.

Creation

VehicleFactory map

type → constructor

Pricing

PricingRegistry

type → PricingStrategy

Channels

NotifierRegistry

name → Notification channel

A growing switch in three classes

If VehicleType appears in a switch in allocation, pricing, and JSON, a new van will miss one. One registry per variation point, or a factory that owns the map.

Global static registry

PricingRegistry.getInstance() makes tests share state and hides the dependency. Pass the registry in. Tests register a fake. Production registers the real strategies.

Unknown keys should fail

A default “hourly” strategy for an unknown type hides a missed registration. Throw. The interviewer wants to see that a new van is an explicit register() call, not a silent fallback. If “unknown means no discount,” that is a Null Object, and you chose that meaning on purpose.

Register before the first request

Do not register from a static initializer on the strategy class. That hides the table and races with tests. main() calls register(). A test method calls register() on a fresh map. After startup, treat the map as read-only.

If you must add a channel at runtime, that is a product feature — still one registry instance passed in, not a new HashMap in the controller. Freeze the table after startup unless the product truly adds plugins at runtime — and even then, one instance, one lock story, tests still construct their own map.

What to draw on the board

Draw a box labeled PricingRegistry with arrows from CAR, BIKE, EV to three strategy boxes. Write register() once at the side. Do not draw a switch. If the interviewer adds a van, point at the box: “one more register().” That is the seam. A factory that still switches internally is a rename.

Prefer an enum or a small string key you control. Do not look up Class names. A typo in a string should throw at register time or at the first forType(), not produce a silent hourly fee. If the key comes from HTTP, validate it against the registry and return 400 — do not let an unknown type fall through to a default strategy.

A default “just use hourly” looks friendly and hides a missed van. Fail the lookup. Register the van. That is the whole follow-up. Write the throw. Do not write a default branch “to be safe.” Safe is an explicit register() the interviewer can see.

If two variation points exist — creation and pricing — draw two tables. One God-map is a switch with extra steps.

A notification channel table

Email, SMS, push. A switch in the notifier and another in the serializer is the vehicle-type smell again. One registry from channel name to Notifier. The notification case study is this table plus a strategy for retry.

java
class ChannelRegistry {
  private final Map<String, Notifier> byName = new HashMap<>();

  void register(String name, Notifier notifier) { byName.put(name, notifier); }

  Notifier require(String name) {
    Notifier n = byName.get(name);
    if (n == null) throw new IllegalArgumentException(name);
    return n;
  }
}

class NotifyService {
  private final ChannelRegistry channels;
  NotifyService(ChannelRegistry channels) { this.channels = channels; }

  void send(String channel, Message message) {
    channels.require(channel).send(message);
  }
}

Registry is not a service locator

A locator is a global bag every class reaches into. A registry is an object you pass in. ParkingLot holds PricingRegistry. It does not call Registries.get("pricing"). Tests construct a small map. That difference is injection, not a naming trick.

Related pages