All posts

ArchitectureSeptember 24, 20266 min read

The integration tax: designing a restaurant stack that survives provider changes

Every point-to-point integration charges a recurring tax in maintenance, outages, and lock-in. A shared domain model and a few proven patterns keep the bill small and make provider swaps routine.

An operations manager unplugs a stack of old order tablets beside one screen that now brings every system together.

A multi-location restaurant stack is rarely designed. It accumulates. A POS here, an online ordering platform there, a loyalty provider, a payment processor, a delivery aggregator, a few marketplaces. Each one arrives with an integration, and each integration is wired directly to whatever it needed to talk to at the time.

That works until it doesn't. The first provider change, the first outage at dinner peak, or the first time finance asks why order totals disagree across systems, you find out what those connections really cost.

Call it the integration tax. You pay it every month, and most brands never see the full bill.

What the tax actually is

The tax has four parts:

  • Maintenance. Every provider changes its API, deprecates fields, and adjusts behavior. Each direct integration needs someone to notice and respond.
  • Outages. A failure in one provider spreads into every system wired to it. Without isolation, a loyalty timeout can block checkout.
  • Lock-in. When business logic lives inside a specific integration, the provider becomes load-bearing. Replacing it means rewriting everything that touches it.
  • Slow swaps. Changing a POS or loyalty provider becomes a long project with a risky cutover, so brands stay with tools they have outgrown.

Point-to-point integrations raise the tax with every connection. Add a new channel and it may need its own link to the POS, loyalty, payments, and delivery. The number of connections grows much faster than the number of systems.

Put a domain model in the middle

The central pattern is an integration layer with a shared domain model. Instead of every system talking to every other system, each one talks to a common model of what the business actually cares about: menus, carts, orders, guests, locations, and loyalty accounts.

Channels

StorefrontMobile appKioskMarketplaces

Shared domain model

MenusCartsOrdersGuestsLocationsLoyalty
Business rules live here

Adapters → providers

ToastOloPunchhStripeDeliverect
Each channel talks to one model. Each provider sits behind its own adapter.

Each provider gets an adapter. The adapter's only job is translation: turning a provider's order format into your order, and your order into what the provider expects. Business rules live in the layer, not in the adapters.

This has three consequences:

  1. Channels stop caring which provider is behind them. Your storefront submits an order. Which POS receives it is an adapter concern.
  2. Provider quirks stay contained. If one POS models combos in an unusual way, that complexity sits in one adapter instead of leaking into every channel.
  3. Swaps become bounded. Replacing a provider means writing and validating one new adapter, not touching every system that depended on the old one.

The domain model is the hard part. It has to be rich enough to represent how the business really works, and stable enough that channels can build on it. The menu is usually the place to start, which we cover in menu as data.

Make every write safe to repeat

Restaurant integrations fail in the messiest possible way: partially. A request times out, but the order was created. A webhook arrives twice. A payment captures, but the POS rejects the ticket.

Three patterns handle most of it.

Idempotency keys

Every write that creates something, such as an order, a payment, or a loyalty redemption, should carry an idempotency key generated by the caller. If the request is retried, the receiving side recognizes the key and returns the original result instead of creating a duplicate. Idempotency is what makes retries safe, and retries are what make integrations reliable.

Same key, same result: a retry never creates a second order
POST /v1/orders
Idempotency-Key: 2f9c61d0-5b8e-4f3a-9d2e-7a1c4e8b0f33
Content-Type: application/json

{ "location": "loc_downtown", "cart": "cart_8812", "fulfillment": "pickup" }

# The first attempt timed out after the order was created.
# The client retries with the same key, and gets the original order back:

HTTP/1.1 200 OK
Idempotent-Replayed: true

{ "id": "ord_1042", "status": "accepted", "location": "loc_downtown" }

Webhooks with retries

Inbound events from providers should be acknowledged quickly, stored, and processed asynchronously. Outbound events should retry with backoff when the destination is slow or down. Assume events arrive late, out of order, and more than once, and design handlers to tolerate all three.

Dead-letter handling

Some events will never succeed on retry: a malformed payload, a missing mapping, a location that was deactivated. Those belong in a dead-letter queue with enough context to diagnose and replay them. A dead-letter queue with no owner is just a slower way to lose orders, so name one.

Reconcile and observe

Even well-built integrations drift. A provider accepts an order but never sends the status update. A refund happens in one system and not the other. A price gets edited directly in a provider's admin.

Reconciliation jobs compare what each system believes on a schedule: orders, payments, loyalty balances, and menu state. Differences get classified. Some are fixed automatically, some are escalated, and all are logged so patterns show up.

Observability makes the whole thing visible. At a minimum, track these per provider and per location:

  • Request volume, error rates, and latency
  • Webhook delivery lag and retry counts
  • Dead-letter queue depth and age
  • Reconciliation mismatches over time

Put them on a health dashboard that operations can read, not only engineers. When a location's orders stop reaching the kitchen, the first person to know should not be a guest.

The goal is not integrations that never fail. It is failures that are contained, visible, and recoverable before a guest notices.

Contracts and flags

Two more practices make change safer.

Contract tests pin down what each adapter expects from its provider and what it promises to the domain model. Run them against provider sandboxes on a schedule, not only at deploy time, so you learn about a breaking change from a failing test instead of a failing lunch rush.

Feature flags control which provider handles which traffic. Scope them by location, channel, or brand. They turn a provider migration from a single switch into a dial you can turn gradually, and turn back.

A runbook for swapping a provider

Here is how a POS or loyalty change can run without a big-bang cutover:

For loyalty, add one step: plan how balances and tier status move, and tell guests before they notice. Trust in points is slow to build and quick to lose.

Questions to ask any vendor

Before signing with a provider, or with any platform that sits between providers, ask:

  • Do write APIs support idempotency keys?
  • How are webhooks retried, and can we replay missed events?
  • Is there a sandbox that behaves like production?
  • How are breaking changes announced, and how long are deprecated versions supported?
  • Can we export all of our data, in a documented format, at any time?
  • Where do incidents get reported, and how quickly?
  • What rate limits apply per location and per brand?
  • If we leave, what does offboarding look like, and what do we keep?

The answers tell you how much tax you are signing up to pay.

The takeaway

The integration tax never appears as a line item, but it shows up everywhere: in engineering time, in dinner-peak outages, and in the provider you keep because leaving feels too risky. A shared domain model with adapters, safe retries, reconciliation, observability, contract tests, and flag-driven migrations turns provider changes from projects into routine operations.

If you want to see what one interface across your POS, order management, loyalty, and payment providers looks like, explore the Techtris API and our integrations, or book a demo and we will map your current stack with you.