dashn9/product-catalog-microservice: A Catalog Service That Writes Like a Ledger
Go, CQRS, exact rational pricing, and a transactional outbox turn product updates into one atomic commit on Google Cloud Spanner.
one atomic bundle, not two separate operations." data-prompt="Create an editorial illustration rendered entirely in black ink on a pure white background. The image depicts a ledger-like product catalog built like a precision machine, with a locked vault at the center, a stamped product card, a price tag, and a sealed outbox envelope all feeding into the same gear train. The artist uses tightly packed crosshatching lines layered at different angles to build up shadow and form, with clean open areas of white for highlights. The line work has the quality of a classic metal engraving, precise and deliberate, with varied line weights where bold contour lines define shapes and finer interior lines create tonal depth. The overall style evokes vintage newspaper editorial illustrations from The Economist or Wall Street Journal. No color, no gradients, no grey fills. Only black lines on white. The background MUST be pure white #FFFFFF. No paper texture, no cream, no off-white, no noise, no grain. Perfectly clean flat white background. No text, no labels, no speech bubbles." loading="lazy">
- This service treats the write path as the product, because the catalog change and the outbox event are committed together in one Spanner transaction.
- Exact rational pricing pushes money into the domain model, which keeps arithmetic precise instead of approximated by floats or loose cents math.
- CQRS here is a practical split, not ceremony, with mutation-heavy writes on one side and direct read models on the other.
- The repo reads like a reference implementation because its boundaries are clearer than its production scaffolding.
The real product is the commit path
The first thing this repo gets right is the write path. A product update is not treated as a simple row change. It is treated as a carefully packed commit that can carry the product mutation and the outbox event together, which is exactly the kind of discipline you want when Google Cloud Spanner is the database of record.
That choice changes everything downstream. The code is not trying to be database agnostic, and it is not pretending that events can be bolted on later. It uses Spanner's atomic mutation model, CQRS, and a change tracker to make consistency the product.
One request, one atomic bundle
- A gRPC handler receives a command such as CreateProduct, ApplyDiscount, or ActivateProduct.
- The use case loads or creates the product aggregate and lets the domain decide what changed.
- The aggregate records dirty fields and domain events instead of forcing a full-row rewrite.
- The committer turns those changes into Spanner mutations for the product table and the outbox table.
- Spanner applies the whole bundle atomically, so the catalog state and the event intent never split apart.
That is the practical payoff of the outbox pattern here. The event is not a second system's opinion about what happened. It is part of the same transaction as the state change, so there is no quiet gap where the catalog says one thing and the event stream says another.
Why money is stored as a fraction
The second surprise is pricing. The repo stores base price as a rational number, not a float, which is a serious way of saying that money is data, not decoration. Instead of trusting binary rounding, it keeps numerator and denominator visible all the way through the domain model.
base_price_numerator = 1999base_price_denominator = 100discount_percentage = 15
That is the trade. You add a little storage complexity, and you get exact arithmetic in return. In a catalog service, that matters more than it first sounds, because pricing errors are easy to miss and painful to unwind. The repo is telling you that the domain should own the math, not the display layer.
Spanner is not just the database, it is the design constraint
The repository layout makes the architecture plain. `internal/app/product/domain` holds the aggregate and value objects, `usecases` orchestrate commands, `queries` own the read path, `repo` handles Spanner-specific persistence, and `internal/pkg/committer` wraps the atomic apply step. That is not a generic ORM shape. It is a system that has learned the shape of its database.
The write side uses a change tracker so the repository can emit targeted mutations instead of rewriting the whole product row. The read side stays direct, querying Spanner into a view model and calculating effective price where needed. CQRS works here because the split is practical, not theatrical. The write model protects invariants. The read model optimizes for retrieval.
The important part is that Spanner changes the cost of carelessness. Once you rely on mutation batches and atomic apply semantics, a loose repository abstraction becomes a liability. This codebase avoids that trap by making Spanner visible in the application shape, not hiding it behind a fake portability layer.
What this replaces in a normal microservice
| Concern | This repo | Typical CRUD service |
|---|---|---|
| Money model | Exact rational values with numerator and denominator | Integers in cents or floating point decimals |
| Write model | Aggregate changes tracked field by field | Full-row updates or ad hoc SQL |
| Event handling | Outbox mutation committed atomically with state | Separate publish step after the database write |
| Database coupling | Spanner shapes the repository and commit plan | Generic SQL layer tries to hide the database |
| Read path | Direct query to a read model | Same model reused for reads and writes |
| Failure mode | Durable intent even when the downstream processor is missing | Lost event or inconsistent state under retry |
| Portability | Lower, because the code speaks Spanner | Higher across common relational databases |
| Precision guarantees | No rounding drift inside the domain model | Rounding leaks into discounts and totals |
Why this reads like a reference implementation
There is no public interview trail here, so the origin story has to be read off the code. The repo looks like a reference implementation for a pattern stack, not a finished product launch. It shows DDD, CQRS, exact pricing, and transactional outbox thinking in one place, then leaves some production extras out of frame.
That absence is informative. The README and tree suggest that the outbox processor is not implemented, and the broader materials do not show a unit test suite, telemetry stack, or deployment polish. That makes the project unusually honest. It teaches the shape of the system before pretending to solve every operational edge case.
The unfinished parts are part of the lesson
The missing processor is not a flaw to hide. It marks the boundary of the example. You can see how to make intent durable inside the transaction, and you can see how the write path stays coherent under Spanner. What you do not yet get is the full delivery machinery that would move those events onward.
That is enough to make the repo valuable. It shows how a catalog service can behave less like CRUD and more like a ledger, where precision, atomicity, and domain boundaries matter more than convenience. If you are trying to understand how Spanner changes application design, this is a compact and useful answer.





