POS

Case study · POS

Food service shipped without adding a single order table

Retail and restaurant point of sale are sold as separate products. This one treats them as one order lifecycle at two speeds, and the claim is falsifiable rather than asserted: when the food service milestone landed, it added no new order tables.

Snapshot

What this isA study project in applied object-oriented design, and a candidate system for the company I work at
BuiltPoint of sale for retail and food service across multiple locations
StackLaravel 13, PostgreSQL, React register, Next.js back office, Tauri v2 with Rust, Docker
TimelineMilestones M0 to M7, one developer
StatusFeature complete and tested. No store has run a trading day on it.

The Question

A business selling both across a counter and at tables buys two systems and reconciles them by hand. The obvious explanation is that the two domains are genuinely different. The question this project set out to answer is whether they are, or whether the industry just ships them separately.

They are the same four steps: open an order, attach lines to it, take money, close it. Retail runs that in sixty seconds, so the open phase is invisible. Food service holds it for an hour, so the open phase gets a name, a screen, and the ability to be handed between staff. Everything else is presentation.

If that is true, one system can serve both and there is nothing to reconcile.

The Approach

Money is a value object, not a number. Amounts live in Domain/Money as integers in the currency's minor unit, with one rounding rule in one place. No float touches a total at any layer. The consequence is that split payments and refunds reconcile to the penny by construction rather than by testing, which is the difference between a cash drawer that balances and one that nearly does.

One system action is one Action class. 168 of them. Each takes an Input DTO, performs one operation, and returns a domain object. A form request never reaches the domain, so the rules that decide what happens to money are not entangled with the shape of an HTTP payload. The practical effect is that "what happens when a cashier voids a line" has one answer in one file.

A monolith, deliberately. One Laravel application, one database, four surfaces on top of it. Splitting a point of sale into services would buy independent deployment and cost distributed transactions across exactly the operations that must not half-happen. For a system where the failure mode is charging someone twice, one transaction boundary is worth more than one deployment boundary.

Tauri and Rust for the register shell. A browser tab cannot open a cash drawer or drive a thermal printer. The desktop shell handles those; the server still decides what a receipt says and whether a drawer may open, and audits who authorised it. The shell turns that into bytes and a pulse and decides nothing.

The Build

Online only, chosen with the cost written down. The terminal requires the server, so the server issues every ID and owns every total. That removes ID allocation, conflict resolution and client schema versioning in one decision. The cost is stated in the design document rather than hidden: a wifi outage stops sales. The mitigation is that every mutating request already carries an idempotency key, committed in the same transaction as the work, which is precisely the groundwork an offline queue would need.

The drawer count is blind. Closing a shift requires the counted cash before the expected total is shown. Showing the expected figure first turns a count into a transcription. It is slower and cashiers dislike it, which is the tradeoff.

One correction worth recording. Payment method screens described offline behaviour the system does not have, and refund-only methods were named as though they could take sales. The interface was making a claim the engineering did not support. Corrected by changing the copy to match the system.

Where It Stands

Order modelOne lifecycle, two selling modes, proved by food service adding no order tables
MoneyInteger minor units end to end, one rounding rule
SurfacesRegister, back office, desktop shell, API
Test coverage550 backend test cases
Trading days runZero

Every statement above is a property of the code, not of a business. No pilot store has opened, so there is no throughput figure, no real variance data and no user feedback, and none is claimed. Load testing is deferred in the roadmap with "first pilot store scheduled" as its trigger.

Next

Built to answer a design question and to be a candidate for a real floor. Get in touch if you run both a counter and tables.

08 · Contact

Let's Build Something Useful

If you're looking for someone to develop business software, improve operational processes, or integrate existing systems, I'd be happy to discuss your project.