image for webhook

Design Patterns I Use in Production

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

Photo by Mark König on Unsplash

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.

Core Idea

Code
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 [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.

Code
return webhooks.stores[store]

Why This Works

Open for extension

Adding a new store is just adding config:

Code
[Store.NewStore]: [ ... ]

No service-layer changes are required.


Data-driven fan-out

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

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

Clean and predictable.


Response handling stays simple

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

Code
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 a HTTP handler, the discriminant is the narrower responseType. This means two different stores could share the same response format without duplicating parsing logic.


Auth is co-located

Each webhook defines its own headers, so the HTTP layer stays generic and reusable.


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.