Episode 9 — System Design / 9.4 — Structural Design Patterns

9.4 -- Interview Questions: Structural Design Patterns

Practice questions with model answers for structural pattern topics commonly asked in software engineering and system design interviews.

How to use this material (instructions)

  1. Read lessons in order -- README.md, then 9.4.a through 9.4.e.
  2. Practice out loud -- 60--120 seconds per question; avoid reading the model answer mid-answer.
  3. Draw diagrams -- interviewers reward clear ASCII or whiteboard diagrams.
  4. Pair with exercises -- use 9.4-Exercise-Questions.md for drills.
  5. Quick review -- skim 9.4-Quick-Revision.md the night before.

Q1. What are structural design patterns and why are they important?

Model answer:

Structural patterns deal with how classes and objects are composed into larger structures. They focus on making these structures flexible and efficient while keeping them decoupled.

There are five key structural patterns:

  • Adapter -- bridges incompatible interfaces
  • Facade -- simplifies complex subsystems
  • Proxy -- controls access to objects
  • Decorator -- adds behavior dynamically
  • Composite -- builds tree-like hierarchies

They are important because real-world systems must integrate third-party services, manage complexity, enforce access control, extend behavior without modifying code, and represent hierarchical data. Structural patterns provide battle-tested solutions to these problems while following SOLID principles, especially the Open/Closed Principle (open for extension, closed for modification).


Q2. Explain the Adapter pattern with a real-world example.

Model answer:

The Adapter pattern allows two classes with incompatible interfaces to work together by wrapping one inside a translation layer.

Real-world example: An e-commerce platform that supports multiple payment gateways. Stripe uses charges.create({ amount: 4999 }) (in cents), PayPal uses payment.execute({ total: '49.99' }) (as a string in dollars), and Square uses createPayment({ amountMoney: { amount: 4999 } }). Each has a completely different API shape.

The Adapter solution:

  1. Define a target interface: PaymentProcessor with processPayment(amount, token)
  2. Create adapters for each vendor: StripeAdapter, PayPalAdapter, SquareAdapter
  3. Each adapter translates our standard call into the vendor-specific API

The client code (checkout service) depends only on PaymentProcessor and never knows which vendor is being used. Switching providers means changing one line of configuration, not rewriting business logic.

Key distinction: The adapter only translates -- it does NOT add new behavior. If you are adding behavior, that is the Decorator pattern.


Q3. What is the Facade pattern and when would you use it?

Model answer:

The Facade provides a simplified interface to a complex subsystem. It does not add functionality; it just makes the subsystem easier to use by hiding internal coordination.

When to use it:

  • A subsystem has many interacting classes (template engine + SMTP + validation + queue + logger for sending an email)
  • Many callers repeat the same multi-step coordination
  • You want to reduce coupling between client code and subsystem internals
  • You are building an API Gateway that aggregates multiple microservices into one endpoint

Example: An EmailFacade that exposes sendWelcomeEmail(user) instead of requiring every caller to load templates, compile them, validate emails, configure SMTP, queue the message, and log the result. The facade coordinates all 6 subsystems; the caller writes one line.

Important caveats:

  • The facade does NOT prevent direct subsystem access -- clients who need fine-grained control can still reach subsystem classes
  • Watch for the God Object anti-pattern: if your facade covers multiple unrelated domains (email + payments + analytics + notifications), split it into focused facades

Q4. Compare Proxy and Decorator. How are they different?

Model answer:

Both Proxy and Decorator wrap an object with the same interface so the client cannot tell the difference. The critical distinction is intent:

AspectProxyDecorator
IntentControl access to an objectAdd new behavior to an object
Who controls creation?Proxy often manages the wrapped object's lifecycleClient typically creates both component and decorator
StackabilitySometimes layered, but not the primary designDesigned for stacking (key feature)
Typical usesLazy loading, caching, auth checking, loggingMiddleware pipelines, adding retry/timeout/logging
Relationship to wrapped"I decide IF you access it""I enhance WHAT it does"

Example of overlap: A "caching proxy" and a "caching decorator" may look identical in code. The distinction is conceptual: the proxy says "I protect you from expensive access," while the decorator says "I enhance this function with caching."

In interviews, acknowledging this overlap shows depth: "The patterns have the same structure but different intentions. In practice, the line can blur, and what matters is clear communication about the purpose."


Q5. How does the Composite pattern work? Give an example.

Model answer:

The Composite pattern represents part-whole hierarchies as trees where individual objects (leaves) and groups (composites) share the same interface. This lets clients treat single objects and collections uniformly.

Three participants:

  1. Component -- the shared interface (e.g., FileSystemEntry with getSize(), display())
  2. Leaf -- an end node with no children (e.g., File)
  3. Composite -- a container that holds children and implements operations by recursively delegating to them (e.g., Directory)

File system example:

  • File.getSize() returns its own size (e.g., 2048 bytes)
  • Directory.getSize() calls getSize() on every child and sums the results
  • The client calls root.getSize() without knowing or caring whether root is a file or a directory containing thousands of nested files

Other examples: menu/submenu navigation, org chart hierarchies, UI component trees (React), pricing bundles containing other bundles, permission groups.


Q6. You need to integrate a third-party API that changes frequently. Which pattern would you use and why?

Model answer:

The Adapter pattern is the right choice.

Reasoning:

  1. Create a stable internal interface that your application depends on (e.g., AnalyticsTracker with trackEvent(name, properties))
  2. Write an adapter (e.g., MixpanelAdapter) that translates your internal interface to the third-party API
  3. When the third-party API changes, you only update the adapter -- zero changes to your application code
  4. If you switch from Mixpanel to Amplitude, you write a new AmplitudeAdapter and swap it in

This approach also improves testability: your tests mock the internal interface rather than the third-party API, so tests never break when the external API changes.

If the third-party service is a complex subsystem with many classes (not just an interface mismatch), you might combine Adapter with Facade to both simplify and translate.


Q7. Explain Express middleware in terms of design patterns.

Model answer:

Express middleware is the Decorator pattern applied to request handling. Each middleware function wraps the next handler in the chain and can:

  1. Execute code before the handler (logging the request, parsing body)
  2. Modify the request or response objects (adding req.user after auth)
  3. Short-circuit the chain (returning 401 without calling next())
  4. Execute code after the handler (logging response time)
Request --> [Logger] --> [CORS] --> [RateLimit] --> [Auth] --> [Handler] --> Response

Each middleware has the same signature (req, res, next), making them stackable in any combination. This is exactly the Decorator pattern's strength: avoid combinatorial explosion of handler variants by composing behaviors at runtime.

The app.use() call is essentially saying "wrap every subsequent handler with this decorator."


Q8. How would you design a caching layer using structural patterns?

Model answer:

I would use the Proxy pattern (or Decorator, depending on intent) to create a transparent caching layer:

// The caching proxy has the same interface as the real service
class CachingUserService {
  constructor(realService, ttl = 60000) {
    this.service = realService;
    this.cache = new Map();
    this.ttl = ttl;
  }

  async getUser(id) {
    const cached = this.cache.get(id);
    if (cached && Date.now() - cached.time < this.ttl) {
      return cached.data; // cache hit
    }
    const user = await this.service.getUser(id); // cache miss
    this.cache.set(id, { data: user, time: Date.now() });
    return user;
  }

  async updateUser(id, data) {
    this.cache.delete(id); // invalidate on write
    return this.service.updateUser(id, data);
  }
}

Key design decisions:

  • Same interface as the real service (drop-in replacement)
  • TTL-based expiration to prevent stale data
  • Cache invalidation on writes
  • The caller does not know whether results are cached or fresh

For a distributed system, I would use Redis as the cache store instead of a Map, add cache-aside or read-through strategies, and potentially use a Facade to coordinate the caching logic with multiple backend services.


Q9. What is the difference between Adapter and Facade?

Model answer:

AspectAdapterFacade
IntentMake one incompatible interface work with anotherSimplify a complex subsystem with many classes
Number of wrapped objectsOneMany (entire subsystem)
Interface relationshipTranslates interface A to interface BCreates a new, simpler interface
The problem"These two cannot talk to each other""This subsystem is too complex for callers"
AnalogyPower plug adapterHotel concierge

They can work together: a Facade might internally use Adapters to bridge incompatible subsystem components.


Q10. Describe a scenario where you would combine multiple structural patterns.

Model answer:

E-commerce product catalog:

  1. Composite -- Products can be individual items or bundles (containing other products/bundles). getPrice() is recursive for bundles.

  2. Adapter -- Product data comes from a third-party supplier API with a different format ({ sku, cost_usd, desc } vs our { id, price, description }). The adapter normalizes the data.

  3. Proxy (caching) -- Frequently accessed products are cached to avoid repeated database/API calls. The caching proxy wraps the product repository.

  4. Decorator -- Cross-cutting concerns: logging every product access, applying regional pricing rules, checking inventory availability. Each decorator wraps the product service.

  5. Facade -- The ProductCatalogFacade exposes simple methods like getProductPage(categoryId, page) that coordinate the product service, search index, recommendation engine, and inventory system.

Architecture flow:

Client
  -> ProductCatalogFacade (simplifies)
    -> LoggingDecorator (logs access)
      -> CachingProxy (caches results)
        -> ProductAdapter (normalizes API data)
          -> Third-party API / Database
    -> Product Composite (recursive pricing for bundles)

Q11. When should you NOT use these patterns?

Model answer:

Adapter: When the interfaces are already compatible (unnecessary layer), when you control both sides and can just make them match, or when the overhead of translation is unacceptable for hot code paths (rare -- usually negligible).

Facade: When the subsystem is already simple (premature abstraction), when clients genuinely need fine-grained control and the facade would restrict them, or when the facade grows into a God Object covering unrelated domains.

Proxy: When direct access is perfectly fine and there is no access control, caching, or lazy loading need. Adding a proxy "just in case" violates YAGNI (You Aren't Gonna Need It).

Decorator: When only one fixed combination of behaviors is ever used (just put the logic directly in the function), when deep decorator stacks make debugging stack traces impossible, or when decorator order sensitivity creates subtle bugs.

Composite: When the data structure is flat (no nesting), when leaves and composites have fundamentally different interfaces (uniform treatment is forced and awkward), or when performance is critical and the recursion overhead matters for millions of nodes.

General principle: Patterns solve specific problems. If the problem does not exist in your codebase, applying the pattern adds complexity with no benefit. "The simplest thing that works" should always be the starting point.


Quick-fire

QuestionShort answer
Does Adapter add behavior?No -- only translates
Can clients bypass a Facade?Yes -- facade is a convenience, not a wall
Does a Proxy change the interface?No -- same interface as the real subject
Can Decorators be stacked infinitely?In theory yes, but deep stacks hurt readability
Does Composite work without recursion?Not meaningfully -- recursion is its superpower

Interview tips

  1. Name the pattern, then explain it. "This is the Adapter pattern. It solves..." shows structure.
  2. Always mention trade-offs. Every pattern has a cost (indirection, complexity, overhead). Acknowledging this shows maturity.
  3. Draw diagrams. Even a rough ASCII diagram of Client -> Adapter -> Adaptee impresses more than a verbal-only answer.
  4. Give real code examples. "Express middleware is the Decorator pattern" or "An API Gateway is a Facade" grounds your answer.
  5. Know the distinctions. Adapter vs Facade and Proxy vs Decorator are the most commonly confused pairs. Prepare crisp one-liners for each.

<- Back to 9.4 -- Structural Design Patterns (README)