Skip to main content

Structural Patterns — Adapter & Decorator

Level: Patterns | Track: Low Level Design

Why it matters in interviews

Adapter and Decorator are the two structural patterns that come up most in LLD interviews because they model real-world constraints: integrating legacy code and extending behavior without modification. Knowing the one-sentence distinction — Adapter changes an interface, Decorator adds behavior through the same interface — lets you answer "how would you add caching/logging/retry to X without modifying X?" instantly.

Core concepts

  • Adapter — wraps an incompatible class so it conforms to an interface the client expects; the wrapped class's interface changes from the client's perspective
  • Object adapter vs class adapter — object adapter uses composition (preferred); class adapter uses multiple inheritance (available in C++, not Java)
  • Decorator — wraps a component that already implements the target interface, adding behavior before/after delegating to it; the interface stays the same
  • Stacking decorators — decorators can be layered: new CachingService(new LoggingService(new UserService())) — each layer adds one responsibility
  • Open/Closed connection — both patterns let you extend behavior (Open) without modifying existing code (Closed); the choice depends on whether the interface matches

Code example

// ===== ADAPTER =====
// Legacy class with a different interface — you cannot modify it
class LegacyLogger {
public void writeLog(String severity, String msg) {
System.out.printf("[%s] %s%n", severity, msg);
}
}

// New interface the rest of the system expects
interface Logger {
void info(String message);
void error(String message);
}

// Adapter: wraps LegacyLogger, exposes Logger
class LegacyLoggerAdapter implements Logger {
private final LegacyLogger legacy;

public LegacyLoggerAdapter(LegacyLogger legacy) { this.legacy = legacy; }

@Override public void info(String message) { legacy.writeLog("INFO", message); }
@Override public void error(String message) { legacy.writeLog("ERROR", message); }
}

// Client code depends only on Logger — no knowledge of LegacyLogger
Logger logger = new LegacyLoggerAdapter(new LegacyLogger());
logger.info("Application started");


// ===== DECORATOR =====
// Base service interface
interface UserService {
User getUser(String id);
void saveUser(User user);
}

// Concrete implementation
class UserServiceImpl implements UserService {
public User getUser(String id) { /* DB call */ return new User(id); }
public void saveUser(User user) { /* DB call */ }
}

// Decorator: wraps UserService, adds request/response logging
class LoggingUserService implements UserService {
private final UserService delegate;
private final Logger logger;

public LoggingUserService(UserService delegate, Logger logger) {
this.delegate = delegate;
this.logger = logger;
}

@Override
public User getUser(String id) {
logger.info("getUser called with id=" + id);
User result = delegate.getUser(id);
logger.info("getUser returned: " + result);
return result;
}

@Override
public void saveUser(User user) {
logger.info("saveUser called: " + user);
delegate.saveUser(user);
logger.info("saveUser completed");
}
}

// Stack a caching decorator on top of the logging decorator:
UserService service = new CachingUserService(
new LoggingUserService(
new UserServiceImpl(), logger
)
);

When to use

PatternUse when...Signal phrase in interviews
AdapterYou have an existing class with the right behavior but the wrong interface"We have a legacy X, can we make it work with the new Y interface?"
DecoratorYou want to add behavior to an object without subclassing and without modifying the original"How would you add caching/logging/retry to X without changing X?"
NeitherYou control the original class and can modify it safelyRefactor instead of wrapping

The golden rule: if the interviewer asks "how would you add caching to the UserService without modifying UserService?" — the answer is always Decorator.

Common interview mistakes

  • Confusing Adapter with Decorator — Adapter bridges two different interfaces; Decorator wraps an object that already implements the same interface; if you're not sure, ask: "does the wrapped object implement the same interface the client expects?"
  • Using inheritance for Decorator — subclassing adds static behavior; Decorator composes at runtime and lets you stack multiple concerns independently
  • Forgetting to delegate — a common bug: writing the logging Decorator but not calling delegate.getUser(id), so the method logs but returns null
  • Adapter when a simple façade would do — if you're wrapping an entire subsystem (not adapting one interface to another), a Façade is the right pattern, not Adapter

Covered by Problems

ProblemLink
Parking Lot System
Ride Sharing System

Resources