E-One

Side project

EoneShop: Microservices, Live

A mini shop built step by step from a single monolith into six services on Cloud Run: a public gateway, four business services that each own a Neon Postgres database and talk to each other only through Google Pub/Sub, and a live feed for the demo. I learned it by building it, and wrote up every step in public as I went.

Watch an order live Code & design notes

How to use the demo

No sign-up, and nothing you do there is real: no money is charged and no email is sent.

  1. Open micro.eonelabs.my (on a laptop the form and the live view sit side by side; on a phone the page scrolls down to the live view for you).
  2. Pick a product and a quantity.
  3. Choose what should happen: a normal order, or one of the three ways to break it.
  4. Click Place order.
  5. Watch the diagram: every dot is a real message sent by one service, through Pub/Sub, to the services that listen for it. A service box lights up when it receives one, and turns red if it keeps failing.
  6. Read the timeline underneath: each line is one step, with the time it happened (measured on the server, counted from when the order was saved).
  7. Keep watching for a few seconds after the order finishes. Clean-up steps, like giving stock back, can still arrive after the order says CANCELLED. The button comes back when it’s quiet.
About 2 seconds

Normal order

Four messages, one after another, ending in COMPLETED.

About 3 seconds

Card declined

CANCELLED, then the cancellation fans out to three services and the stock is released.

About 20 seconds

Payment too slow

Nothing seems to happen, then the order times out. The late payment is never charged.

About 20 seconds

Catalog is down

Catalog turns red after five failed tries, then the order times out.

The first order after a quiet spell can take a few extra seconds: the services and their databases sleep when nobody is using them, and have to wake up first. Your scenario only ever affects your own order, never anyone else’s.

Break it on purpose

On the live page you place an order and watch each event travel through Pub/Sub in real time: order placed, stock reserved, payment charged, order confirmed. Four scenarios are on offer, and each one only ever affects your own order:

What is a “saga”? (in plain words)

I’m still new to microservices, and “saga” was one of the words that confused me most at the start, so here is how I understand it today.

Think of booking a holiday: a flight, a hotel and a rental car, each from a different company. If the car booking fails at the end, nobody magically cancels everything for you. You have to cancel the hotel and the flight yourself, one by one. A saga is exactly that, written down as a rule: every step has a matching “undo” step, and when something fails, the steps that already happened are undone.

Why is it needed here? When a shop is one app with one database, a failed order can be rolled back in a single go. EoneShop is split into services that each keep their own data, so there is no single “undo everything” button. Each service has to clean up its own part.

Step

Catalog reserves the stock

The item is set aside for you, so nobody else can buy the last one.

Its undo

Catalog gives the stock back

If the order is cancelled later, the item goes back on the shelf.

Step

Payment charges the card

The money is taken, but only after the stock is safely reserved.

Its undo

Payment refunds the card

If the order is cancelled after paying (for example it was too slow), the money goes back.

So when a card is declined, the order is cancelled and one message goes out: “this order is cancelled”. Catalog hears it and gives the stock back. Payment hears it too, and would refund if it had already charged. On the live page you can watch this happen, step by step.

There are two ways to run a saga, and I picked the simpler one for a shop this size:

For the technical readers: a choreography-based saga with compensating transactions, a PENDING status acting as a semantic lock, and tombstones for events that arrive out of order. The details are in part 4 of the series.

How it's built

Architecture diagram: a browser calls the public gateway service, which calls four private services (order, catalog, payment, notification), each with its own Neon database, all connected through a central Pub/Sub topics hub; a feed service taps the topics, and Cloud Trace collects a trace across every service. Browser Gateway public Pub/Sub topics Order Catalog Payment Notification private, ID tokens DB DB DB DB Feed (tap) Cloud Trace

The six services

Each runs as its own Cloud Run service with its own service account. The four business services never call each other: they only publish and react to events through Pub/Sub, so any one of them can be slow or down without the others waiting on it.

Edge · public

gateway

The only public service. Serves the live page, forwards /api calls to the private services with an ID token, and rate-limits each visitor. Owns no data.

Business · own DB

order

Takes orders (with an idempotency key), announces order.created, and alone decides how each order ends: completed, or cancelled after a failure or a timeout.

Business · own DB

catalog

Products and stock. Reserves stock when an order arrives and gives it back when the order is cancelled. A tombstone stops a late event from reserving stock for a dead order.

Business · own DB

payment

A fake card provider. Charges once the stock is reserved, never charges the same order twice, and refunds if the order is cancelled after it paid.

Business · own DB

notification

“Emails” the customer when an order completes or is cancelled. Exactly one email per order, even when Pub/Sub delivers the same event twice.

Observer · read-only

feed

Listens to every topic and the dead-letter topic, and keeps recent events in memory so the live page can animate them. Nothing depends on it.

Around them: 10 Pub/Sub topics (one per event type, plus a dead-letter topic), four Neon databases, and Cloud Trace. Two more services exist only for running it on a laptop: a small Pub/Sub stand-in and a trace collector.

Seven parts, written up

  1. Why I Started With a Monolith
  2. When a Function Call Becomes a Network Call
  3. Events, the Outbox, and Getting Every Message Twice
  4. Undo Buttons, Timeouts, and Breaking Things on Purpose
  5. One Trace per Order, and the Bug It Found
  6. Leaving the Laptop
  7. Watch It Live (and the Bug It Caught)

Bugs it caught

Stack

FastAPI SQLAlchemy Google Pub/Sub Cloud Run Neon Postgres OpenTelemetry Cloud Trace Cloud Build pytest