Let’s build an izakaya-style ordering app!

During my last trip to Tokyo, I would often stumble upon a very specifically Japanese way of ordering food. A popular one is the vending machines that dispense tickets instead of the actual food or drinks — you’ll typically find this in ramen bars. There’s also the conveyor belt sushi bar that tallies up your order price based on the colors of the sushi plates you got. At first these were pretty confusing as a tourist, but my very limited Japanese speaking skills and Kanji knowledge helped us to not embarrass ourselves too much and get the food we wanted.

Apart from these more “manual” ways of ordering, an interesting system I’d see from time to time - especially at bars and izakayas (basically a Japanese pub) - are QR codes on a table that would lead to a web app where you could see the whole menu, make orders, and ask for the bill. Typically this menu has a sidebar with categories for different kinds of food, appetizer, drinks, and so on. You can order food items from each category, sometimes throw in add-ons (ex. extra soy sauce and/or spring onions), and see the list of items order as well as the running total of the bill. Anyone on the table could place individual orders anytime. When it’s time to pay for the bill, some places would ask you to settle it at the cashier near the exit, while others would allow you to pay in cash at the table or card through the app.

It’s worth pointing out here that these apps are not really “pretty” in the first place. They have that dated early 2000’s internet look with text that sometimes pushes elements to the side and fonts and colors which don’t quite feel curated. But despite that, the app just worked. We could understand how to navigate the app easily and place orders conveniently. I could also feel that that kind of system helped the restaurants and bars manage orders in a highly systematic way (as most things are in Japan, anyway).

It’s a great example of the saying “simplicity is elegance”. Feeling inspired, I thought I would pirate the idea, build a similar app from scratch, and establish dominance over the global food and beverage industry. But as I don’t quite have the time and energy to start this as a new business (let alone figure out if people would actually adopt and use the software), I thought it would be an interesting idea to at least work through architecting the system and speccing out how it should work.

Let’s build an izakaya-style ordering app!

Approach

When coming up with a completely new technical project (and to a certain extent new features on existing ones) I generally approach it in these steps:

  1. Define the problem/s and the requirements to solve it - preferably as a Minimum Viable Product, or MVP.
  2. Identify the different kinds of users, roles, and responsibilities.
    1. You could argue that this falls under problem definition, but I’ll keep it in a separate section to give it more attention.
  3. Define the database schema and other necessary implementation details (business logic, tech stack).
    1. It’s hard to split this up since decisions on business logic can affect the schema and vice versa. The same goes for our choice of database(s) and how we plan to deploy this.
  4. Define actions and interfaces - APIs, pages, and other finer implementation details.

We’ll go over these steps in the following sections. For this article, we’ll treat this as a system design exercise for building an MVP. We won’t write any actual code (which is the meat of the work in every software project) but rather plan out the architecture and behaviors for the app.

We’re also consciously omitting a lot of “important” stuff such as user account management in favor of focusing on the core business logic specific to the ordering app. There are also some small design decisions here and there that could have been approached in different, probably more scalable ways, but again we’re sticking to a nice and simple MVP for now.

With that in mind, let’s start defining our requirements.

MVP specifications

For the MVP, we just want something that isn’t painful to use for both the restaurant and the customer. At least one establishment in the world — even your friend’s mom and pop shop comfort food restaurant — should be happy to try it out. We’ll go with these requirements:

  • The restaurant can list menu items that customers can order in their browser (no app installation). Each menu item should have these details: name, price, picture, description.
  • The restaurant can set up categories to group the items under. Customers would be browsing through these category pages and order items on those pages.
  • An order should be unique to a table and the customers sitting on the table at that time.
  • Different people on the same table should be able to use the web app and add to the order.
  • The table should be able to view the current order - items, quantities, individual price, and total price.
  • The restaurant should be able to receive incoming orders per table and track which order items have been served or not.
    • I believe many businesses prefer using “tickets” or orders physically printed on paper. You also can’t use a touchscreen or press buttons if your hands are greasy. We won’t worry about the actual printer but just assume that it can receive order item data from our app via a webhook.
  • The restaurant should have the data to produce a receipt when it’s time to pay for the order.
    • For the sake of simplicity, let’s say the official receipt and payment is handled by another system that can receive the order data from our app via a webhook.

Beyond the MVP, these are other nice-to-have features I’ve either noticed in some restaurants or thought up myself. We’ll cover these separately after we’ve put the MVP together.

  • Different restaurant owners can sign up and manage their own establishments
    • To take this a step further - restaurant owners can add different moderators with varying access permissions
  • “Submit order” button — the table’s order isn’t sent to the kitchen until one of the customers on the table presses that button which sends the table’s running order in a “batch” to the kitchen.
  • Menu items may have add-ons — ex. extra soy sauce, spring onions (select one or multiple)
  • Customer-facing multi-language support
  • Happy hour promos — ex. 50% on select items from 2-6PM
    • You can go crazy with this - buy 1 take 1, so on
  • Special discounts — PWD, senior citizen, loyalty card
  • Pay at the table via credit card or other payment gateways
  • Dashboards, analytics, AI & ML 🚀 (half joking but half true, stick around till the end)

Who are the users, and how will they use the app?

Now that we have our spec, it would be useful to ask ourselves who the different kinds of users are and how they’ll use the app. This will help us figure out things like access, kinds of interfaces (either on screen or on paper), and capabilities that would likely affect how to design the schema.

Let’s start with the obvious:

  • Customer — places order, eats food, gets wasted on Super Drys and highballs, pays for bill
  • Restaurant — sets up and manages menu, receives orders, prepares food, serves food, receives payment

Seems like we’re on the right track, but we’re could do better. From my experience of actually sitting down at the restaurant and observing how the staff operates, it’s clear that the staff play different roles. There are waiters who serve your food, some of whom handle the order payment, and there are chefs who prepare the food. But there’s also the management and back-office staff that set up the menu you interact with. These observations give us valuable insights on the different kinds of roles at play.

Here are our new roles and responsibilities (which feel a little bit like user stories):

  • Customer — sits on table, places order, eats food
  • Restaurant manager — sets up and manages menu
  • Restaurant waiter — serves food, receives payment
  • Restaurant chef — receives orders, prepares food

There are of course some edge cases to the way we’ve grouped things together (ex. the person that prepares your food can be the same one that serves it to you, especially if you’re sitting at the counter) but this should cover most bases.

With these groupings, let’s try coming up with the different interfaces and capabilities that make sense to our app:

  • Customer interface — sits on table, place orders, see items ordered
  • Manager interface — CRUD for managing the menu categories and items
  • Cashier interface — retrieve a table’s order and prepare all details for payment (as an MVP, not our system’s responsibility)
  • Chef interface — print and track incoming order items

Now that we have a clearer idea of what we want to track and who should see it, it’s time to put the tech stack together.

Picking the tools for the job

Web client & server

All we need for our ordering app is a web client for users to interact with on their phones / tablets and a web server to handle requests from the client. The server would also be responsible for sending data to external systems — the order ticketing printer and the payment / point-of-sale machine.

The framework and language of choice boils down to preference. You can do a server-side Ruby on Rails thing or a NodeJS + Express backend with a Vue frontend thing. It doesn’t really matter if we decide between one or two repositories (or a monorepo with multiple services) as long as we have a client that can make requests to a server that interacts with our database. I would err on the side of choosing the frameworks you’re most productive with unless you have a strong reason to do so otherwise.

There are also questions such as going REST vs GraphQL; we’ll stick with the former for this exercise.

Database

We need somewhere to save our menu items and table orders, so we need a persisted database which our web server will interact with.

Most of the time, anything that runs on SQL (MySQL, Postgres, etc.) would be a safe bet because of their sheer popularity and having been battle-tested in countless production apps. NoSQL tools such as MongoDB and Firestore are also worth considering. We could also throw in Redis if we want to support message queues when receiving orders.

In favor of keeping things simple, let’s roll with a good old SQL database since our data is relational (a menu has menu items, an order has order items, etc.) and call it a day before we prematurely over-optimize.

Image storage

Since we want to save pictures of the menu items, we’d have to use another service for this - AWS S3, Google Cloud Storage, Imgix, and Cloudinary are some examples. Whichever one is used is an implementation detail; for now we just need to know that this is a separate service from our database.

At this stage, we have enough information to put together a nice diagram that clearly illustrates our service and how they’ll interact with the users.

Untitled

Not yet covered here are details like what data will be sent between which services and who can access what. From here, we’ll piece it together gradually starting with shaping our data — that is, building the schema.

Building the schema

Let’s skim over the interfaces and their capabilities. We now know the things we want to track and save to our database - orders, menu items, and so on. These things, nouns, are often a close representation of what the database’s tables and collections will look like. If we were to list down these nouns mentioned in the capabilities, we’d have:

  • orders
  • order items
  • menu categories
  • menu items
  • tables*
  • payments*
  • restaurants*

Looks like everything’s covered! Some systems might want to track the actors involved (ex. who served your table, who the cashier conducting the payment is) but we’ll skip that for our MVP.

We can still refine the schema a bit more. To our system, tables in a restaurant are usually just numbers and aren’t anything much more special, so we don’t necessarily need another database table for that. We can keep it as a field under orders — order.table_number (int) .

Sidenote: Different groups can use the same table number at different times of the day; differentiating these as different orders in the database is an implementation detail that will be explained later on.

Payments could be in its own table; doing so makes it more easily extensible, such as if we want to support credit card payments at the table. For now, let’s pretend that the restaurant handles the payment outside the system and all they need to do is mark an order as paid or not. We can then work with order.is_paid (boolean) or, better yet order.status - "UNPAID" | "PAID" (in case other funky stuff like refunds or voiding orders comes later on).

If we want to support multiple businesses (see: multitenant architecture), we could approach this by having a restaurants table under which all other orders and menu items would be nested under (ex. order.restaurant_id). Let’s forget about this for our MVP so we can instead focus on how the other tables relate to each other without adding too much mental overhead.

Putting these relationships together, here’s what our schema would look like at this stage of planning (with some fields such as create / update timestamps omitted):

Untitled

Sidenote: I wasn’t sure how to present this step-by-step since schema design is the kind of thing that slowly comes together all at once; things get shuffled around till they make sense, often just intuitively. There’s probably a better way to explain it, but hopefully this makes sense given the amount of context we currently have.

With our very simple schema, we can see how the data we want to store looks like and how it relates to other data points. All menu items should belong under a category. When someone orders a menu item, we save it as an order item under the table’s order and note the quantity of the order. So when you’re ordering sushi for example, you create one order item that points to the sushi menu item.

You might notice that we’re missing other fields part of our requirements such as total order price or order item price. It’s true that we want to know what these values are, but they don’t need to be saved in the database as they can be calculated — for example, order_item.price = order_item.quantity * order_item.menu_item.price and order.price = SUM(order.order_items, order_item => order_item.price). It makes sense in practice to save these prices and other details upon payment, especially given that menu prices can change and items can be deleted, but for the sake of simplicity let’s assume that those things don’t happen.

Alright! So we have our database schema roughly together. It’s time to look into how we’ll get our different users to interact with the database.

Speccing out APIs and webhooks

We’ll keep things at the level of business logic since code implementation is too much to cover in the planning phase.

Menu actions

EndpointBehavior
GET /api/menu_categoriesReturn an array of menu categories and their attributes (title, description).
Note: querying all of these would be the “whole restaurant menu”
GET /api/menu_categories/:idReturn the category attributes (title, description).
POST /api/menu_categoriesCreate a new menu category with provided attributes (title, description).
PUT /api/menu_categories/:idUpdate a menu category with provided attributes (title, description).
DELETE /api/menu_categories/:idDelete a menu category.
GET /api/menu_categories/:id/menu_itemsReturn an array of menu items and their attributes (title, description, price, image).
POST /api/menu_categories/:id/menu_itemsCreate a new menu item with provided attributes (title, description, price, image URL).
PUT /api/menu_categories/:id/menu_items/:idUpdate a menu item with provided attributes (title, description, price, image URL).
DELETE /api/menu_categories/:id/menu_items/:idDelete a menu item.

These endpoints will be used by…

  • Manager UI - with all endpoints, managers can build up their list of menu items which customers can order.
  • Customer UI - with only the GET endpoints, customers can view menu categories and items when navigating the web app UI and picking what to order.

Order actions

As mentioned before, we have to solve the problem of differentiating orders across the same table number. For example, when one group walks into the restaurant and starts making orders on table 7, a new row in the orders table should be created and all order items should belong under that order. Once that group pays the check and leaves, another group coming in on table 7 would have a new order row in the table.

Our system is thankfully unrealistically simple, so we don’t have to worry too much. Let’s say a customer scans the QR on table 7. This would render the menu page to the user, with the table number saved somewhere. The URL would do, such as restaurant.com/menu?table_number=7 . (Caveat: this way someone could easily screw up their own order or mess with someone else’s order. Let’s leave that problem alone for this exercise.)

Let’s say the customer browses through the menu and finally picks something to order. Here’s the funky part — when creating an order item, create a new order if there isn’t one that matches order.table_number = 7 and order.status = "UNPAID" and create the order item under that new order. Otherwise, create the order item with the unpaid order under table 7.

This works since each table can’t have more than 1 order at a time, and we’re assuming that all orders eventually move from “UNPAID” status to “PAID”. This means we’ll never have 2 “UNPAID” orders under the same table number at any given time. It would be safe to add some business logic in the app to ensure that this doesn’t happen.

EndpointBehavior
GET /api/ordersReturn an array of orders and their attributes (title, description).
GET /api/orders/:idReturn the order attributes (status, total price, order items).
Note: we’re skipping GET /api/orders/:id/order_items since you could reason that we’d always want to get the items when retrieving order details.
PUT /api/orders/:idUpdate an order’s attributes (at the moment, only status).
DELETE /api/orders/:idDelete an order.
GET /api/order_by_table_number/:table_numberReturn the order attributes (status, total price, order items) given the table_number instead of the order. This would retrieve the only UNPAID order for that table number, if any.
POST /api/order_by_table_number/
:table_number/order_items
Create a new order item under a table_number. The order_id would be determined by the logic we covered in the above section. Other attributes: menu_item_id, quantity
DELETE /api/order_by_table_number
/:table_number/order_items/:id
Delete an order item.
POST /api/order_by_table_number/
:table_number/send_to_payment_machine
Sends a webhook to the payment machine with all necessary order information (more details below).

Sidenote: This could be put together with Swagger or be written in more detail such as the expected request and response bodies.

These endpoints will be used by…

  • Cashier UI - with all the endpoints, cashiers can manage customer orders and delete / amend data.
  • Customers UI - with the endpoints GET /api/order_by_table_number/:table_number and POST /api/order_by_table_number/:table_number/order_items, customers can view their ongoing order at the table and add items to it.
    • Note: I intentionally left out PUT and DELETE endpoints for the customer as there are race condition situations such as the customer canceling an order after it’s already been prepared for them. Let’s assume that only the restaurant staff should have the access to edit or delete an order item in case, for example, the customer makes a mistake while ordering.

Ah, but what about the payment machine and order ticketing printer? As mentioned in the requirements, we’ll assume that all they need is to receive a message via webhooks.

WebhookBehavior
→ order item data → order ticketing printerWhen triggered by POST /api/orders_by_table_number/:table_number/order_items, sends the menu item, quantity, and table number to the order ticketing printer. The order must have an UNPAID status and a matching table number.
→ order data → payment machineWhen triggered by POST /api/order_by_table_number /:table_number/send_to_payment_machine, sends the order attributes (status, total price, order items) to the payment machine. The order must have an UNPAID status and a matching table number.

Again, we won’t cover the details of our API interfacing with these external services in this high level spec.

Congratulations, we have a plan for our MVP!

That’s more or less it! From the above diagrams (architecture and schema) and the tables of endpoints and behaviors (menu, orders, and webhooks), this plan as it is fulfills our MVP specifications. We can officially say that we’ve taken our first step to total control of the global food & beverage industry.

P.S. As for the nice-to-haves mentioned earlier, we’ll cover them probably in another article. Thank you for reading!

← blog