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.

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 appKioskMarketplacesShared domain model
Adapters → providers
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:
- Channels stop caring which provider is behind them. Your storefront submits an order. Which POS receives it is an adapter concern.
- 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.
- 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.
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.


