
Restro
A Go and MySQL restaurant app: server-rendered menus, browser-side carts, table allocation and an explicit order lifecycle.
Timeline
Ongoing
Role
Backend Developer
Team
Solo
Status
CompletedTechnology Stack
Key Challenges
- Keeping table allocation and order writes consistent
- Separating authentication from permission checks
- Defining which order status changes are allowed
Key Learnings
- Reusing parsed Go templates across requests
- Modelling orders and line items in MySQL
- Finding transaction boundaries in a multi-step write
Restro handles a restaurant workflow: a customer chooses items, submits an order
and gets a table number. Staff move that order through cooking, service and
billing. The implementation uses Go, Gorilla Mux, html/template and MySQL,
with JavaScript for the cart and status buttons.
Rendering pages without a frontend framework
At startup, cmd/main.go loads configuration, parses the templates, initializes
sessions and connects to the database before starting the router. The
template loader
collects page templates, admin pages, partials and components into one
template.Template. Handlers reuse it through ExecuteTemplate.
GET /menu reads category and search parameters, fetches items and renders
menu.html. Search replaces the category result when a search term is present;
it does not combine both filters. The response includes the menu data, so the
browser does not need another request to populate the page.
The cart stays in localStorage. JavaScript handles quantity changes and extra
instructions, then sends item IDs and quantities to POST /order. The server
looks up prices in MySQL to calculate the total. A price shown in the browser
is therefore not the value used to create the order.
After submission, the handler stores the order ID and table ID in a Gorilla session and returns a URL for the browser to navigate to. This mixes ordinary HTML navigation with small JSON endpoints where an interaction needs them.
One order, several database writes
The schema separates orders from order_item. An order stores its customer,
table, instructions, status, total and timestamp. Each line item stores an item
ID and quantity, with a composite primary key on the item and order IDs.
AddOrder reads available tables, chooses the first result and marks it unavailable. It then inserts the order and its line items. These are separate database calls, without a transaction around the sequence.
That leaves two concrete failure cases. Concurrent requests can select the same table before either marks it occupied. A failed insert can also leave a table occupied without a complete order. Go can serve requests concurrently, but that does not make this sequence atomic.
A change I would make here is to reserve the table and write the order inside a transaction, using a locking read or a conditional update to prevent two requests from claiming it. Line-item insert failures would then roll back the order and the reservation together. This is a proposed change, not behavior the current implementation provides.
The order lifecycle has a point of no return
Orders start as placed. The transition validator allows changes between
placed, cooking and served, including moving backwards. Those states can
also move to billed. Once billed, an order can only become paid; paid orders
cannot change state again.
That distinction supports correcting a preparation status while keeping the billing boundary explicit. The PATCH handlers check that the order exists, read its current status and validate the requested transition before writing. Marking an order paid also makes its table available again.
The validation and update are separate queries, though. Two requests can both validate an old status before either writes. A conditional update against the expected status would let the database detect that conflict.
The chef template displays only placed and cooking orders. The admin order view can filter by status, but its renderer still fetches the full matching set. There are pagination hooks in the template; database pagination is unfinished.
Authentication and staff permissions
Passwords are hashed with bcrypt at cost 10. Login issues an HS256 JWT in a
token cookie. Protected routes validate it, check that the user still exists
and put the token's user data in the request context.
The Go role constants are customer, admin and super. Registration makes
the first user a super user. Although the SQL enum includes chef, the chef
page is registered under /admin, rather than behind a separate chef role.
The routing also exposes an implementation gap: order status endpoints sit on
the authenticated /order router, without the admin middleware. The
admin middleware
redirects a non-admin but does not return before calling the next handler.
Those checks need to be fixed before describing the app as having complete
role-based enforcement.
Running it and measuring it
Docker Compose starts MySQL and the Go application. It waits for the database
health check before starting the app; the entrypoint applies SQL migrations
with golang-migrate and then executes the server. The database connection pool
allows 25 open connections and five idle connections, with a five-minute maximum
connection lifetime.
The repository includes ApacheBench scripts for registration and the admin order page, configured for 1,000 requests at concurrency 10. They give me a starting workload to investigate. They do not establish a speedup over Express: there is no equivalent Express implementation or comparison harness in the repository.
For the next pass, I would measure latency and failed requests while checking that every successful order has its line items and a valid table reservation. A quick response is useful only if the order it represents is complete.