Case study
- Role
- Software Developer
- Organisation
- Greggs PLC
Greggs Kiosk
The customer-facing self-service screen in Greggs shops, running on Linux. Customers browse the full menu, build and customise an order, see allergen and nutrition information, pay, and collect against an order number. It was a flagship project on the Greggs 2025 roadmap.

01 / Overview
What I did.
I joined as one of the first developers and shaped the early prototype that the customer-facing features were built on, including the meal deal builder and the upselling flow. I later led development of reusable order-sharing, loyalty, promotions and analytics packages, published internally and now consumed by the POS and drive-through teams, along with a shared models library that keeps tills and backend services in agreement.
02 / Architecture
Layers listed outermost first.
How it's put together.
A single Flutter application on Linux, deliberately thin: the behaviour that other Greggs surfaces also need lives in versioned packages rather than in the kiosk. The kiosk was first to need loyalty and order sharing, but it was not going to be the only one, so those were built as libraries from the outset.
Kiosk application
The screen the customer touches.
Flutter on Linux, driving the attract screen, menu browsing, customisation, basket, payment and the order-number handoff. Includes an accessibility mode that brings the interactive controls down into the lower half of the screen so they are within reach.
Shared feature packages
Written once, consumed by several teams.
Order sharing, loyalty, promotions and analytics as separately versioned packages published to an internal registry. POS and drive-through consume the same builds, so a fix to loyalty is a version bump rather than three separate patches.
Shared models library
One definition of an order.
The contract between kiosk, tills and backend services lives in one library both sides depend on, so the shape of an order cannot quietly diverge between the thing that creates it and the thing that fulfils it.
Backend services
Menu, basket, allergens, payment.
The .NET services behind the screen. Requests go out through a single HTTP layer so timeouts, retries and error surfacing are handled in one place rather than per feature.
Local test doubles
Frontend work that does not wait on an environment.
Allergen data served from Wiremock behind NGINX locally, so interface work could continue against a stable, known response set instead of a shared upstream environment.
Build & device delivery
Getting it onto the hardware.
Containerised builds through Azure DevOps, deployed to Linux kiosk hardware in shops.
03 / Decisions
Each of these had a credible alternative. The trade-off is stated, not hidden.
Choices, and what they cost.
Shared behaviour as versioned packages, not copied code
- Chose
- Internal Dart packages on a private registry, consumed by kiosk, POS and drive-through
- Instead of
- Each team implementing loyalty and order sharing themselves
- A shared source folder copied between repositories
- Why
- Loyalty and order sharing are rules, not screens, and rules that exist in three codebases will be subtly different in three codebases within a year. A published, versioned package means one implementation and one place to fix a bug, and it lets each consuming team upgrade on their own schedule instead of being blocked by ours.
- Trade-off
- It demands release discipline. A breaking change now needs a major version and a migration path for teams you do not control, which is slower than editing your own code.
A shared models library across frontend and backend
- Chose
- One library defining the order contract, depended on by both sides
- Instead of
- Each application declaring its own DTOs
- Generating clients from a schema per consumer
- Why
- The expensive bug in a system with tills, kiosks and a backend is not a broken screen. It is two components that disagree about what an order contains and only find out in a live shop. A single shared definition makes that disagreement a compile error.
- Trade-off
- It couples release trains together. Changing the model means coordinating a release across components that would otherwise ship independently.
Flutter for the kiosk surface
- Chose
- One Flutter codebase targeting the Linux kiosk hardware
- Instead of
- A native Linux application
- A web application in a kiosk-mode browser
- Why
- The interface is highly custom and animation-heavy, and Flutter renders its own widgets rather than inheriting platform ones, so the design is identical on every unit and can be reused on other surfaces later. It also let one team cover the whole front end rather than splitting by platform.
- Trade-off
- Anything genuinely device-specific (payment terminals, receipt printers, kiosk peripherals) needs a platform channel, which is more work than calling a native API directly.
Mocking allergen data locally rather than pointing at the real service
- Chose
- Wiremock behind NGINX serving allergen responses on the developer machine
- Instead of
- Consuming the live upstream service during development
- Hard-coded fixtures inside the application
- Why
- Allergen data is exactly the kind of dependency that blocks a team: shared, slow-moving, and something you must not get wrong. A local mock gives every developer the same known responses, including the awkward ones, without competing for a shared environment, and unlike in-app fixtures it exercises the real HTTP path.
- Trade-off
- A mock drifts from the real contract unless somebody deliberately refreshes it, and a mock that is out of date is worse than no mock at all.
04 / Stack
Grouped by the job they do rather than listed as a keyword run.
Technologies.
- Application
- Services
- Platform
- Delivery
- Tooling
05 / Gallery
Select any image to view it full size.
How it looks.
Interface
Attract screen: idle state, inviting the first touch. First-time guidance: order, wait, collect. Reachable from the attract screen. Menu browsing. Category rail, calories on every card, basket total always visible. Accessibility mode: the whole interface drops into the lower half of the screen so every control is within reach. Customisation, one of the first flows built on the prototype. Allergen and nutrition detail. The data was mocked locally during development. Basket, set as a receipt. Handoff: the order number the customer collects against.
In shop
Live in shop. A customer ordering. The end of the journey. Behind the scenes.
06 / Outcome
Where it landed.
The kiosk launched into live shops, reaching three locations in the first week with two to six units each, and the rollout continued from there on feedback from customers, shop teams and stakeholders. The packages I led (order sharing, loyalty, promotions and analytics) were adopted by the POS and drive-through teams, so the work outlived the surface it was written for.