Most e-commerce and SaaS platforms treat the order as the atomic unit: it's created, paid, fulfilled, closed. A restaurant or bar doesn't work that way. The atomic unit is the shift, and an order is just one event that happens somewhere inside it, alongside a dozen other orders on other tabs, opened and closed and modified by different staff on different terminals before anyone settles anything.
Software built around the transaction gets the happy path right and falls apart on everything that actually defines the business: a bartender who opens twelve tabs before closing any of them, a server who splits a check four ways after the food already fired, a manager who voids a drink an hour after it was poured because the guest complained, a cash drawer that has to reconcile against every terminal that touched it during an eight-hour window, not against any single sale.
This is the same failure mode we've seen across retail and hospitality builds: a domain expert describes the shift, the handoff, the drawer count, and the tip pool, and the vendor demo shows a checkout button. The gap between those two things is where restaurants end up keeping a paper log next to a $10,000 POS system.
Why the shift, not the order, is the real aggregate root
A shift has a start time, an assigned till or drawer with a starting float, a set of staff clocked into it, and an end time where everything gets reconciled: cash counted, card batches settled, tips calculated and pooled, and a variance reported against the expected drawer total. Every order that happens inside that window belongs to the shift first and to a table or tab second.
That ordering matters for three reasons that generic order-first software gets wrong:
- Voids and comps need shift-level authority, not order-level authority. A manager override on a $40 void is meaningless if the system can't tell you which manager, on which shift, authorized how many of them that week.
- Tips are calculated against the shift, not the check. Pooled tips split across servers, hosts, and bussers require every check in the shift to be closed and attributed before the pool total means anything.
- The drawer doesn't reconcile against a transaction — it reconciles against the shift. Cash in, cash out, card settlement, comps, and the starting float all net out at shift close, not at checkout.
A schema that puts the shift where it belongs
The practical fix is to make the shift a first-class row that every order, void, and cash movement foreign-keys into, rather than deriving a fake shift boundary later from timestamps. A minimal version looks like this:
create table shift (
id uuid primary key,
location_id uuid not null,
till_id uuid not null,
opened_by uuid not null,
opened_at timestamptz not null,
closed_by uuid,
closed_at timestamptz,
starting_float numeric(10,2) not null,
counted_total numeric(10,2),
variance numeric(10,2)
);
create table order_ticket (
id uuid primary key,
shift_id uuid not null references shift(id),
table_or_tab text,
server_id uuid not null,
opened_at timestamptz not null,
closed_at timestamptz
);
create table cash_movement (
id uuid primary key,
shift_id uuid not null references shift(id),
kind text not null check (kind in ('sale','refund','payout','drop')),
amount numeric(10,2) not null,
created_at timestamptz not null
);
Nothing here is exotic. The point is that shift_id is a required, non-nullable foreign key on every money-moving row. If your schema lets an order or a cash movement exist without a shift, you will eventually have a shift close that can't explain a $60 drawer variance, and no query will tell you why. We've written about this exact class of problem for money-holding systems generally in reconciliation, ledgers, and why a float isn't money — a restaurant drawer is a float with extra steps.
Concurrency inside the shift is the actual hard problem
A single shift on a busy Friday might have six terminals, four servers, and a bartender all writing to overlapping tabs at once. Two servers can attempt to split the same check simultaneously. A modifier fired to the kitchen five seconds ago can't be silently dropped because a second terminal edited the same ticket. This isn't a UI problem, it's a concurrency problem, and it needs the same discipline you'd apply to any system with concurrent writers: optimistic locking on the ticket row, an append-only event log for modifications, and a clear rule for what happens when two edits collide (last-write-wins is usually wrong here; the kitchen already fired, so the system needs to reconcile against what was sent, not just what was typed).
This is also where offline resilience earns its keep. Connectivity drops don't pause a Friday night rush, and a POS that can't take orders without a live connection to the server is a POS that loses the shift's most profitable two hours. We built exactly this pattern for Retailer OS: terminals write locally first and reconcile against the shift ledger once the connection returns, using the same queue-and-replay approach we describe in field service apps that keep working with no signal. The domain is different, but the mechanism — local write, queued sync, conflict resolution against a canonical log — is identical.
Where the transaction model is actually fine
None of this means every hospitality build needs shift-level modeling on day one. A single-terminal counter-service coffee shop with one till and no tip pool genuinely doesn't need it — the order really is the atomic unit there, because there's no handoff, no pooled tip, and no multi-terminal drawer to reconcile. Building the full shift model for that business is over-engineering that slows down a simple MVP for no operational benefit.
The rule of thumb: if the business has more than one person touching cash or more than one terminal per drawer during a service window, model the shift as the aggregate root from the start. If it's a single owner-operator on a single till, ship the simpler transaction model and revisit later. Retrofitting a shift boundary onto a schema that was built around bare transactions is a full data migration, not a feature flag — every historical order, void, and cash movement needs a shift assigned retroactively before your reporting means anything.
The parts that generic software still gets wrong
Beyond the shift boundary itself, a handful of restaurant-specific rules trip up software built for retail or generic commerce:
- 86'd items need to propagate to every terminal and the online ordering menu in the same second, not on the next sync cycle, or you sell something the kitchen can't make.
- Modifiers change the price and the prep time, and both need to be attached to the line item at order time, not derived later from a lookup table that might change.
- Split checks aren't just dividing a total — they need to preserve which items each guest actually ordered for tax and comp accuracy, not just an even split of the bill.
- Tip pooling formulas vary by role and by shift, and hard-coding one formula into the checkout flow means a rewrite the first time management changes the split.
Each of these is small in isolation and expensive in aggregate, which is exactly the pattern across every vertical we build for — the rules that look like edge cases from a generic platform's point of view are the whole job from the operator's point of view.
The order is one event inside the shift; software that forgets which one is the aggregate root eventually can't explain its own cash drawer.
If you're evaluating whether to buy an off-the-shelf POS or build the shift model correctly from the start, that decision is worth working through before you commit to either — see what we build and how we scope and price engagements, or talk to us directly about the specific shift, tip, and drawer rules your operation actually runs on.
Frequently asked
Why does restaurant POS software need a shift model instead of just tracking orders?
Because cash drawers, tip pools, and manager voids are all reconciled at the end of a shift, not per order. Without a shift as the aggregate root, you can't explain a drawer variance or split pooled tips accurately across staff who worked overlapping hours.
Can a small restaurant skip the shift model and just use a simple POS?
Yes, if there's one terminal, one till, and one person handling cash per service window. The shift model earns its complexity once you have multiple terminals or staff sharing a drawer, where reconciliation actually requires a shift boundary.
How do restaurants handle offline POS during an internet outage?
Terminals should write orders locally first and queue them for sync, so service continues uninterrupted. When connectivity returns, the queued orders reconcile against the shift ledger, the same pattern used in offline-first field service and retail systems.
What's the hardest technical problem in restaurant POS software?
Concurrency during a rush: multiple terminals editing the same tab, splitting the same check, or firing modifiers to the kitchen at nearly the same moment. It needs optimistic locking and an append-only edit log, not just a database transaction per order.
How do tip pools get calculated correctly in POS software?
Tip pool formulas should reference the shift, not individual checks, since pooling splits total tips across roles like servers, bussers, and hosts based on hours worked or points earned during that specific shift, and the formula itself needs to be configurable, not hard-coded.