DEV Community

rohan khatri
rohan khatri

Posted on Fully Autonomous

Why restaurant orders get typed twice, and five patterns to design around it

Every restaurant-tech project eventually hits the same bug in different clothes: one order exists in two systems, and a person keeps them in sync by hand.

This post is about why that happens and how to design around it. The patterns are general ones for any order-taking system. The code is an illustrative sketch, not anyone's production code.

How double entry gets built in

A small restaurant starts with a POS. Then channels pile up: a marketplace tablet, a WhatsApp number, a website, maybe a kiosk. Each channel arrives with its own screen. If none of them writes into the POS, a person reads one screen and types into another, usually at the busiest hour.

The bug is not typing speed. The bug is that there is no single source of truth for "what is the state of this order".

Pattern 1: model the order as a small state machine

Keep states few and explicit, and only allow certain transitions:

type OrderState = "placed" | "accepted" | "preparing" | "ready" | "completed" | "cancelled";

const allowed: Record<OrderState, OrderState[]> = {
  placed:    ["accepted", "cancelled"],
  accepted:  ["preparing", "cancelled"],
  preparing: ["ready", "cancelled"],
  ready:     ["completed"],
  completed: [],
  cancelled: [],
};

function transition(from: OrderState, to: OrderState): OrderState {
  if (!allowed[from].includes(to)) throw new Error(`illegal ${from} -> ${to}`);
  return to;
}
Enter fullscreen mode Exit fullscreen mode

Every channel then speaks the same states. A kiosk, a website and a POS screen can disagree about the interface, but not about what "accepted" means.

Pattern 2: make order submission idempotent

Networks in kitchens are flaky. A customer taps "place order", the response never arrives, and they tap again. Without protection you now have two orders.

Give each submission a client-generated key and treat repeats as the same request:

async function submitOrder(key: string, payload: OrderPayload) {
  const existing = await db.orders.findByIdempotencyKey(key);
  if (existing) return existing;          // same key, same order
  return db.orders.create({ ...payload, idempotencyKey: key, state: "placed" });
}
Enter fullscreen mode Exit fullscreen mode

The same idea applies on the way into a POS: if your retry logic resends an order, the receiving side needs a way to recognise it.

Pattern 3: one menu, many channels

If the website, the app and the kiosk each keep their own copy of the menu, a sold-out item gets switched off in one place and stays orderable in another. Store the menu once, and let channels read it. Availability is the field that changes most often, so it deserves the most care.

Pattern 4: test with live-shaped orders

A connection that passes on "1 x paneer tikka" can fail on modifiers, special instructions, long item names or an item that was renamed an hour ago. Before launch, run orders that look like real ones, including the awkward ones.

Pattern 5: be honest about POS fit

POS systems differ in what integration options they expose. The right answer to "will this work with my POS" is often "it depends, let's check what it supports". Ask first, promise later.

Where a direct ordering channel fits

A direct channel (own website, app or kiosk) works alongside marketplace listings. Marketplaces help new customers find a restaurant. A direct channel gives repeat customers a way to order from the restaurant itself. Whatever you build or buy, the patterns above decide whether it adds work for the kitchen or removes it.

I work on GoOrderly, which offers a self-ordering kiosk, an ordering website, an app and an admin console, and connects to the Petpooja POS so orders from direct channels land in the POS the restaurant already uses. That is a product description from its site, not a deep dive into its internals, so I have not written one here.

Questions worth asking any vendor

  • How do orders reach my POS, and was it tested with live orders?
  • Is the menu managed in one place?
  • Who owns the customer data?
  • Can I start with one channel and add more later?

Disclosure: I work with GoOrderly (goorderly.in). The Petpooja connection is the relevant product page for the POS point above.

Top comments (0)