Behavioral patterns solve how objects interact and pass behavior. These four show up constantly in LLD problems: rate limiters, pub-sub systems, undo/redo, and vending machines.
Strategy Pattern
What it does: Encapsulate a family of algorithms, make them interchangeable, and let the client pick one at runtime.
Where it appears in interviews: Rate limiting, sorting, filtering, discount calculations, encryption.
Example: Rate Limiter with Pluggable Strategies
interface RateLimitStrategy {
boolean allowRequest(String userId);
}
class TokenBucketStrategy implements RateLimitStrategy {
private final Map<String, Integer> tokens = new HashMap<>();
private final int capacity;
private final int refillRate; // tokens per second
public TokenBucketStrategy(int capacity, int refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
}
@Override
public boolean allowRequest(String userId) {
int current = tokens.getOrDefault(userId, capacity);
if (current > 0) {
tokens.put(userId, current - 1);
return true;
}
return false;
}
}
class SlidingWindowStrategy implements RateLimitStrategy {
private final Map<String, Queue<Long>> windows = new HashMap<>();
private final int maxRequests;
private final long windowMs;
public SlidingWindowStrategy(int maxRequests, long windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
}
@Override
public boolean allowRequest(String userId) {
long now = System.currentTimeMillis();
Queue<Long> queue = windows.computeIfAbsent(userId, k -> new LinkedList<>());
while (!queue.isEmpty() && queue.peek() < now - windowMs) {
queue.poll();
}
if (queue.size() < maxRequests) {
queue.add(now);
return true;
}
return false;
}
}
class RateLimiter {
private final RateLimitStrategy strategy;
public RateLimiter(RateLimitStrategy strategy) {
this.strategy = strategy;
}
public void handleRequest(String userId) throws TooManyRequestsException {
if (!strategy.allowRequest(userId)) {
throw new TooManyRequestsException();
}
// process request
}
}
// Usage: swap strategies without touching RateLimiter
RateLimiter limiter = new RateLimiter(new TokenBucketStrategy(10, 5));
limiter.handleRequest("user123");
// Later: switch to sliding window
RateLimiter limiter = new RateLimiter(new SlidingWindowStrategy(100, 60000));
Observer Pattern
What it does: Define a one-to-many dependency between objects. When one object changes, notify all dependents automatically.
Where it appears: Pub-sub systems, event buses, listener patterns, notifications.
Example: EventBus for Ride-Sharing
interface RideEventListener {
void onRideCreated(Ride ride);
void onRideAccepted(Ride ride);
void onRideCompleted(Ride ride);
}
class RideEventBus {
private final List<RideEventListener> listeners = new ArrayList<>();
public void subscribe(RideEventListener listener) {
listeners.add(listener);
}
public void unsubscribe(RideEventListener listener) {
listeners.remove(listener);
}
public void publishRideCreated(Ride ride) {
for (RideEventListener listener : listeners) {
listener.onRideCreated(ride);
}
}
public void publishRideAccepted(Ride ride) {
for (RideEventListener listener : listeners) {
listener.onRideAccepted(ride);
}
}
public void publishRideCompleted(Ride ride) {
for (RideEventListener listener : listeners) {
listener.onRideCompleted(ride);
}
}
}
class NotificationService implements RideEventListener {
@Override
public void onRideCreated(Ride ride) {
sendNotification("Ride created: " + ride.getId());
}
@Override
public void onRideAccepted(Ride ride) {
sendNotification("Driver accepted: " + ride.getDriver().getName());
}
@Override
public void onRideCompleted(Ride ride) {
sendNotification("Ride complete. Rating: pending");
}
private void sendNotification(String message) {
// send via email/SMS/push
}
}
class AnalyticsService implements RideEventListener {
@Override
public void onRideCreated(Ride ride) {
logEvent("ride_created", ride.getId());
}
@Override
public void onRideAccepted(Ride ride) {
logEvent("ride_accepted", ride.getId());
}
@Override
public void onRideCompleted(Ride ride) {
logEvent("ride_completed", ride.getId());
}
private void logEvent(String event, String rideId) {
// log to analytics service
}
}
class RideService {
private final RideEventBus eventBus;
public RideService(RideEventBus eventBus) {
this.eventBus = eventBus;
}
public void createRide(Ride ride) {
// create logic
eventBus.publishRideCreated(ride);
}
public void acceptRide(Ride ride, Driver driver) {
// acceptance logic
ride.setDriver(driver);
eventBus.publishRideAccepted(ride);
}
public void completeRide(Ride ride) {
// completion logic
eventBus.publishRideCompleted(ride);
}
}
// At startup:
RideEventBus eventBus = new RideEventBus();
eventBus.subscribe(new NotificationService());
eventBus.subscribe(new AnalyticsService());
RideService rideService = new RideService(eventBus);
Command Pattern
What it does: Encapsulate a request as an object so you can parameterize clients, queue requests, and support undo/redo.
Where it appears: Undo/redo stacks, macro recording, request queuing, transactions.
Example: Undo/Redo for a Document Editor
interface Command {
void execute();
void undo();
}
class InsertTextCommand implements Command {
private final Document document;
private final int position;
private final String text;
public InsertTextCommand(Document document, int position, String text) {
this.document = document;
this.position = position;
this.text = text;
}
@Override
public void execute() {
document.insertAt(position, text);
}
@Override
public void undo() {
document.deleteRange(position, position + text.length());
}
}
class DeleteTextCommand implements Command {
private final Document document;
private final int start, end;
private String deletedText; // saved for undo
public DeleteTextCommand(Document document, int start, int end) {
this.document = document;
this.start = start;
this.end = end;
}
@Override
public void execute() {
deletedText = document.getRange(start, end);
document.deleteRange(start, end);
}
@Override
public void undo() {
document.insertAt(start, deletedText);
}
}
class Document {
private StringBuilder text = new StringBuilder();
public void insertAt(int position, String s) {
text.insert(position, s);
}
public void deleteRange(int start, int end) {
text.delete(start, end);
}
public String getRange(int start, int end) {
return text.substring(start, end);
}
public String getContent() {
return text.toString();
}
}
class Editor {
private final Document document = new Document();
private final Stack<Command> undoStack = new Stack<>();
private final Stack<Command> redoStack = new Stack<>();
public void insertText(int position, String text) {
Command cmd = new InsertTextCommand(document, position, text);
cmd.execute();
undoStack.push(cmd);
redoStack.clear(); // clear redo on new action
}
public void deleteText(int start, int end) {
Command cmd = new DeleteTextCommand(document, start, end);
cmd.execute();
undoStack.push(cmd);
redoStack.clear();
}
public void undo() {
if (!undoStack.isEmpty()) {
Command cmd = undoStack.pop();
cmd.undo();
redoStack.push(cmd);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command cmd = redoStack.pop();
cmd.execute();
undoStack.push(cmd);
}
}
public String getContent() {
return document.getContent();
}
}
// Usage:
Editor editor = new Editor();
editor.insertText(0, "Hello");
editor.insertText(5, " World");
editor.undo(); // "Hello"
editor.redo(); // "Hello World"
State Pattern
What it does: Allow an object to change its behavior when its state changes. Encapsulate state-specific logic in separate classes.
Where it appears: Vending machines, traffic lights, order workflows, game entities (idle, running, paused).
Example: Vending Machine
interface VendingMachineState {
void insertMoney(int amount);
void selectItem(String itemCode);
void dispense();
}
class IdleState implements VendingMachineState {
private final VendingMachine machine;
public IdleState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(int amount) {
machine.addBalance(amount);
machine.setState(new ReadyState(machine));
}
@Override
public void selectItem(String itemCode) {
System.out.println("Insert money first");
}
@Override
public void dispense() {
System.out.println("No item selected");
}
}
class ReadyState implements VendingMachineState {
private final VendingMachine machine;
public ReadyState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(int amount) {
machine.addBalance(amount);
}
@Override
public void selectItem(String itemCode) {
Item item = machine.getItem(itemCode);
if (machine.getBalance() >= item.getPrice()) {
machine.setSelectedItem(item);
machine.setState(new DispenseState(machine));
} else {
System.out.println("Insufficient balance");
}
}
@Override
public void dispense() {
System.out.println("Select an item first");
}
}
class DispenseState implements VendingMachineState {
private final VendingMachine machine;
public DispenseState(VendingMachine machine) {
this.machine = machine;
}
@Override
public void insertMoney(int amount) {
System.out.println("Transaction in progress");
}
@Override
public void selectItem(String itemCode) {
System.out.println("Transaction in progress");
}
@Override
public void dispense() {
Item item = machine.getSelectedItem();
machine.dispenseItem(item);
int change = machine.getBalance() - item.getPrice();
if (change > 0) {
machine.returnChange(change);
}
machine.setState(new IdleState(machine));
}
}
class VendingMachine {
private VendingMachineState state;
private int balance;
private Item selectedItem;
private Map<String, Item> inventory;
public VendingMachine() {
this.state = new IdleState(this);
this.balance = 0;
}
public void insertMoney(int amount) {
state.insertMoney(amount);
}
public void selectItem(String itemCode) {
state.selectItem(itemCode);
}
public void dispense() {
state.dispense();
}
public void setState(VendingMachineState state) {
this.state = state;
}
public void addBalance(int amount) {
balance += amount;
}
public int getBalance() {
return balance;
}
public Item getItem(String code) {
return inventory.get(code);
}
public void setSelectedItem(Item item) {
this.selectedItem = item;
}
public Item getSelectedItem() {
return selectedItem;
}
public void dispenseItem(Item item) {
System.out.println("Dispensing: " + item.getName());
}
public void returnChange(int amount) {
System.out.println("Returning change: " + amount);
}
}
In the interview
-
Recognize the smell, name the pattern. "If we add a third pricing strategy, the if-else will explode. I'll use Strategy pattern and define PricingStrategy interface."
-
Know when each shines: Strategy for algorithms, Observer for events, Command for undo/redo or queuing, State for workflows.
-
Don't over-engineer. If there's only one strategy or one state, keep it simple. Patterns shine when there are multiple implementations.