SE

Structural patterns

Must-know concept1.3 hIntermediate

Repository, Adapter, Decorator, Facade.

Structural patterns describe how objects compose. They are the answer to "I have a thing that almost does what I need — how do I adapt, decorate, or front it without rewriting?"

The big idea

Structural patterns solve interface mismatch. You have an existing object and you need it to look or behave slightly differently — to fit a different interface, to gain a behaviour, to hide a complex subsystem. Each pattern attaches that change outside the original object so the original stays untouched.

Adaptertranslate interfaces
Decoratorwrap to add behaviour
Facadesimplify a complex API
Repositoryhide persistence

Repository — hide where data lives

The repository wraps your data source (DB, API, file) behind an interface shaped around your domain — not around SQL.

interface UserRepository {
  findById(id: number): Promise<User | null>;
  save(user: User): Promise<void>;
  search(q: string, page: number): Promise<User[]>;
}
 
// PG implementation
class PgUserRepository implements UserRepository { /* SQL inside */ }
 
// In-memory implementation for tests
class FakeUserRepository implements UserRepository { /* dict inside */ }

Adapter — make X look like Y

You have a LegacyPaymentClient whose signature is wrong for your Payment interface. Wrap it.

interface Payment { charge(cents: number): Promise<void>; }
 
class LegacyPaymentClient {                          // can't change this
  pay(amountDollars: number, currency: string) { /* … */ }
}
 
class LegacyPaymentAdapter implements Payment {
  constructor(private readonly legacy: LegacyPaymentClient) {}
  async charge(cents: number) {
    this.legacy.pay(cents / 100, 'USD');
  }
}

The rest of the system never sees the legacy client.

Decorator — wrap to add a capability

Decorators wrap an object with the same interface, intercepting calls.

class CachingUserRepository implements UserRepository {
  private cache = new Map<number, User>();
  constructor(private readonly inner: UserRepository) {}
 
  async findById(id: number) {
    if (this.cache.has(id)) return this.cache.get(id)!;
    const user = await this.inner.findById(id);
    if (user) this.cache.set(id, user);
    return user;
  }
  // pass-through for other methods
  save(u: User)            { return this.inner.save(u); }
  search(q: string, p: number) { return this.inner.search(q, p); }
}
 
const repo = new CachingUserRepository(new PgUserRepository());

Stack decorators (logging + caching + retries) without touching the original class.

Facade — one front door to a complex subsystem

When a job requires coordinating five clients, a facade hides the choreography:

class CheckoutFacade {
  constructor(
    private readonly orders: OrderService,
    private readonly payments: PaymentService,
    private readonly inventory: InventoryService,
    private readonly notify: NotificationService,
  ) {}
 
  async checkout(cartId: number, paymentMethod: string) {
    const order = await this.orders.draftFromCart(cartId);
    await this.inventory.reserve(order);
    await this.payments.charge(order.total, paymentMethod);
    await this.orders.confirm(order.id);
    await this.notify.emailReceipt(order);
    return order;
  }
}

API gateways and BFFs (backend-for-frontend) are facades at a different scale.

In practice

The structural pattern you will reach for most is Repository + Decorator: domain code calls a repository, and you stack decorators (cache, log, retry) around it. Adapter shows up at every system boundary you don't fully control. Facade is the cure for "this code calls eight clients and I can't read it."

Key takeaways

  • Structural patterns solve interface mismatch — change the outside, not the inside.
  • Repository abstracts persistence; the tests-vs-production swap is the immediate win.
  • Adapter wraps a wrong-shape object to fit the interface you want.
  • Decorator stacks behaviour around an object that keeps the same interface.
  • Facade puts one front door on a complex subsystem; gateways are facades at scale.

Checkpoint questions

Use these to test whether the lesson is clear enough to explain without rereading.

  1. 1What does a Repository hide from domain code?
  2. 2When would you use an Adapter instead of changing the caller or provider?
  3. 3How does Decorator add behavior without changing the wrapped object?
  4. 4What complexity should a Facade hide, and what should it leave visible?

References

External resources for going deeper after the lesson above.