25 March 2026

Design patterns I use in production

A config-driven approach to building scalable, maintainable webhook systems without conditional complexity.

When I recently built a webhook system for multiple stores, I wanted something scalable without filling the codebase with conditionals. Different stores had different needs — some required multiple webhooks, others just one. Instead of hardcoding logic, I moved everything into a configuration object and let the system execute based on data.

The approach

Core idea

The entire per-store webhook config lives in a single object:

webhooks: {
  stores: {
    [Store.StoreOne]: [
      {
        url: `url1`,
        authHeaders: { ... },
        responseType: 'storeone'
      },
      {
        url: `url2`,
        authHeaders: { ... },
        responseType: 'management'
      }
    ],
    [Store.StoreTwo]: [
      {
        url: `url2`,
        authHeaders: { ... },
        responseType: 'management'
      }
    ]
  }
}

Using computed property names like [Store.StoreOne] keeps keys in sync with the enum and avoids fragile string literals. With Record<Store, ...>, TypeScript ensures only valid stores are used. This turns what could be conditional logic into a simple lookup:

return webhooks.stores[store];
Why it works

Four properties that make this pattern hold up

Open for extension

why this matters

Adding a new store is just adding config — no service-layer changes required:

[Store.NewStore]: [ ... ]

The system picks it up automatically. There's no conditional to update, no switch statement to extend, no risk of forgetting a branch.

Data-driven fan-out

why this matters

The system doesn't care how many webhooks a store needs. It just loops over whatever the config says:

const results = await Promise.all(
  configs.map((c) => {
    return doSomething;
  })
);

Clean and predictable. A store with one webhook and a store with five are handled identically.

Response handling stays simple

why this matters

Instead of branching on store, responses are handled by type:

const response = await fetch();

if (responseType === "management") {
  // additional check for response details
  // throw if response details not matching with conditions
}

Rather than branching on store inside an HTTP handler, the discriminant is the narrower responseType. This means two different stores can share the same response format without duplicating parsing logic.

Auth is co-located

why this matters

Each webhook entry defines its own authHeaders, so the HTTP layer stays generic and reusable. There's no auth-switching logic scattered across the codebase — the config is the single source of truth for how each endpoint authenticates.


Closing

Takeaway

Move behaviour into data.

It keeps your codebase easier to extend, easier to read, and much harder to break as new integrations are added. The pattern scales naturally — the more stores or webhook types you add, the more the config-driven approach pays for itself compared to a conditional-heavy alternative.