Skip to content

The Order Repository

migrations/order/0001_init.sql (the orders, order_items, and outbox tables) and services/order/internal/repo/orders.go — an OrderRepo whose Create method inserts an order, its line items, and an outbox row describing the event, all inside one pgx.Tx. Then services/order/internal/server/server.go — a real Server implementing all three OrderServiceServer methods, replacing The gRPC Server →‘s placeholder and finally putting its injected Catalog client to work: CreateOrder looks up each item’s current price from Catalog before ever touching the repository.

OrderRepo.Create writes to three tables — orders, order_items, and outbox — and all three writes share a single transaction on purpose. An order, the line items that make it up, and the fact that this specific event happened are one atomic unit of truth: if a crash landed between inserting the order and inserting its outbox row, the order would exist in the database with no record anywhere that order.created should ever be published — every downstream consumer (Payment, in Order Saga → — Module 9) would simply never learn this order exists. One transaction makes that failure mode structurally impossible: either all three writes land, or none do. The Transactional Outbox → is where the reasoning behind writing an event to a database table instead of publishing straight to Kafka gets its own full explanation — this lesson just writes the code that makes the single-transaction guarantee real.

Pricing belongs in the Server layer, not the repository, for the same reason validation did in The Product API →: OrderRepo.Create takes already-priced items ([]PricedItem) and has no idea Catalog even exists. Deciding how to get a price — a live gRPC call, in this course’s case — is an API-layer decision the repository shouldn’t need to know about.

One pgx.Tx wrapping the order insert, every item insert, and the outbox insert

  • Pros: atomic by construction — there is no code path where an order is saved without its items, or saved without the event that will eventually announce it; a single defer tx.Rollback(ctx) after Begin means any error anywhere in the function — a bad insert, a JSON marshal failure, anything — safely undoes everything already written in this call.
  • Cons: the transaction now holds row locks across three tables for its whole duration instead of one, which is a longer critical section than a single-table insert; a JSON marshal failure for the outbox payload (unlikely, but possible if a future field isn’t serializable) now fails order creation entirely, coupling the reliability of event serialization to the reliability of placing an order at all.

codes.FailedPrecondition for a product Catalog reports as not found, rather than codes.InvalidArgument

  • Pros: the request itself is syntactically valid — product_id is a well-formed string — the problem is a fact about current system state (no such product exists right now), which is exactly what codes.FailedPrecondition means in gRPC’s own status-code guidance, distinct from a malformed request. A client can tell the two apart: “you sent something the server can never accept” (InvalidArgument) versus “what you sent might be valid depending on state” (FailedPrecondition).
  • Cons: the distinction is subtle and inconsistently applied across real-world APIs — a caller genuinely has to know gRPC’s status-code conventions to branch differently on the two, and plenty of clients just treat both as “bad request” and move on regardless.
create extension if not exists pgcrypto;
create table orders ( id uuid primary key default gen_random_uuid(), customer_id text not null, status text not null default 'pending', total_cents bigint not null, created_at timestamptz not null default now() );
create table order_items ( id bigserial primary key, order_id uuid not null references orders(id) on delete cascade, product_id uuid not null, quantity int not null, unit_price_cents bigint not null );
create table outbox ( id uuid primary key default gen_random_uuid(), aggregate_id uuid not null, event_type text not null, payload jsonb not null, created_at timestamptz not null default now(), published_at timestamptz );
create index on outbox (published_at) where published_at is null;

Save this as migrations/order/0001_init.sql and apply it:

Terminal window
migrate -path migrations/order -database "$ORDER_DB_URL" up

A few shape decisions worth noting now (the outbox table gets its own dedicated explanation in The Transactional Outbox →):

  • status is stored as lowercase text'pending', 'confirmed', 'cancelled' — not the OrderStatus proto enum. The database doesn’t know protobuf exists; Server’s toProtoStatus (below) is the one place that maps between the two.
  • order_items.order_id references orders(id) on delete cascade means deleting an order (this course never does, but a future admin tool might) automatically removes its line items — no orphaned rows to clean up by hand.
  • outbox.aggregate_id is the id of whatever row this event is about — every order-related event in this course points back to an orders.id, which is what lets a future consumer (or a debugging query) find “every event for this order” with one where aggregate_id = $1.
// Package repo is the PostgreSQL-backed store for the Order service.
package repo
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Item is the row shape of a single order_items row.
type Item struct {
ProductID string
Quantity int32
UnitPriceCents int64
}
// Order is the row shape of the orders table, with its items attached.
type Order struct {
ID string
CustomerID string
Status string
TotalCents int64
Items []Item
CreatedAt time.Time
}
// PricedItem is a line item whose unit price has already been looked up
// (from the Catalog service) before Create is called.
type PricedItem struct {
ProductID string
Quantity int32
UnitPriceCents int64
}
// OrderRepo is the PostgreSQL-backed store for orders.
type OrderRepo struct {
db *pgxpool.Pool
}
// New returns an OrderRepo backed by db.
func New(db *pgxpool.Pool) *OrderRepo {
return &OrderRepo{db: db}
}
type createdEventItem struct {
ProductID string `json:"product_id"`
Quantity int32 `json:"quantity"`
UnitPriceCents int64 `json:"unit_price_cents"`
}
type createdEventPayload struct {
OrderID string `json:"order_id"`
CustomerID string `json:"customer_id"`
TotalCents int64 `json:"total_cents"`
Items []createdEventItem `json:"items"`
}
// Create inserts a new pending order, its line items, and an "order.created"
// outbox row in a single transaction — all three commit together or none do.
func (r *OrderRepo) Create(ctx context.Context, customerID string, items []PricedItem) (*Order, error) {
var total int64
for _, it := range items {
total += int64(it.Quantity) * it.UnitPriceCents
}
tx, err := r.db.Begin(ctx)
if err != nil {
return nil, fmt.Errorf("repo: begin create order tx: %w", err)
}
defer tx.Rollback(ctx)
var o Order
err = tx.QueryRow(ctx, `
insert into orders (customer_id, status, total_cents)
values ($1, 'pending', $2)
returning id, customer_id, status, total_cents, created_at`,
customerID, total,
).Scan(&o.ID, &o.CustomerID, &o.Status, &o.TotalCents, &o.CreatedAt)
if err != nil {
return nil, fmt.Errorf("repo: insert order: %w", err)
}
eventItems := make([]createdEventItem, 0, len(items))
for _, it := range items {
if _, err := tx.Exec(ctx, `
insert into order_items (order_id, product_id, quantity, unit_price_cents)
values ($1, $2, $3, $4)`,
o.ID, it.ProductID, it.Quantity, it.UnitPriceCents,
); err != nil {
return nil, fmt.Errorf("repo: insert order item: %w", err)
}
o.Items = append(o.Items, Item{ProductID: it.ProductID, Quantity: it.Quantity, UnitPriceCents: it.UnitPriceCents})
eventItems = append(eventItems, createdEventItem{ProductID: it.ProductID, Quantity: it.Quantity, UnitPriceCents: it.UnitPriceCents})
}
payload, err := json.Marshal(createdEventPayload{
OrderID: o.ID,
CustomerID: o.CustomerID,
TotalCents: o.TotalCents,
Items: eventItems,
})
if err != nil {
return nil, fmt.Errorf("repo: marshal order.created payload: %w", err)
}
if _, err := tx.Exec(ctx, `
insert into outbox (aggregate_id, event_type, payload)
values ($1, 'order.created', $2)`,
o.ID, payload,
); err != nil {
return nil, fmt.Errorf("repo: insert outbox row: %w", err)
}
if err := tx.Commit(ctx); err != nil {
return nil, fmt.Errorf("repo: commit create order tx: %w", err)
}
return &o, nil
}
// Get returns the order with the given id, items included. The returned
// error wraps pgx.ErrNoRows (checkable with errors.Is) when no such order
// exists.
func (r *OrderRepo) Get(ctx context.Context, id string) (*Order, error) {
var o Order
err := r.db.QueryRow(ctx, `
select id, customer_id, status, total_cents, created_at
from orders
where id = $1`, id).Scan(&o.ID, &o.CustomerID, &o.Status, &o.TotalCents, &o.CreatedAt)
if err != nil {
return nil, fmt.Errorf("repo: get order %s: %w", id, err)
}
items, err := r.itemsForOrder(ctx, id)
if err != nil {
return nil, err
}
o.Items = items
return &o, nil
}
// ListByCustomer returns every order placed by customerID, most recent
// first, items included.
func (r *OrderRepo) ListByCustomer(ctx context.Context, customerID string) ([]Order, error) {
rows, err := r.db.Query(ctx, `
select id, customer_id, status, total_cents, created_at
from orders
where customer_id = $1
order by created_at desc`, customerID)
if err != nil {
return nil, fmt.Errorf("repo: list orders: %w", err)
}
defer rows.Close()
var orders []Order
for rows.Next() {
var o Order
if err := rows.Scan(&o.ID, &o.CustomerID, &o.Status, &o.TotalCents, &o.CreatedAt); err != nil {
return nil, fmt.Errorf("repo: scan order: %w", err)
}
orders = append(orders, o)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("repo: iterate orders: %w", err)
}
for i := range orders {
items, err := r.itemsForOrder(ctx, orders[i].ID)
if err != nil {
return nil, err
}
orders[i].Items = items
}
return orders, nil
}
// itemsForOrder returns every order_items row for orderID, in insertion
// order. Shared by Get and ListByCustomer so both fetch items the same way.
func (r *OrderRepo) itemsForOrder(ctx context.Context, orderID string) ([]Item, error) {
rows, err := r.db.Query(ctx, `
select product_id, quantity, unit_price_cents
from order_items
where order_id = $1
order by id`, orderID)
if err != nil {
return nil, fmt.Errorf("repo: list order items: %w", err)
}
defer rows.Close()
var items []Item
for rows.Next() {
var it Item
if err := rows.Scan(&it.ProductID, &it.Quantity, &it.UnitPriceCents); err != nil {
return nil, fmt.Errorf("repo: scan order item: %w", err)
}
items = append(items, it)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("repo: iterate order items: %w", err)
}
return items, nil
}
type statusEventPayload struct {
OrderID string `json:"order_id"`
Status string `json:"status"`
}
// UpdateStatus moves an order to status ("confirmed" or "cancelled") and
// writes the matching "order.confirmed"/"order.cancelled" outbox row in the
// same transaction. The Order saga (Module 9) is what calls this once it
// learns the payment result.
func (r *OrderRepo) UpdateStatus(ctx context.Context, id, status string) error {
eventType := map[string]string{
"confirmed": "order.confirmed",
"cancelled": "order.cancelled",
}[status]
if eventType == "" {
return fmt.Errorf("repo: update order status: no outbox event for status %q", status)
}
tx, err := r.db.Begin(ctx)
if err != nil {
return fmt.Errorf("repo: begin update status tx: %w", err)
}
defer tx.Rollback(ctx)
tag, err := tx.Exec(ctx, `update orders set status = $1 where id = $2`, status, id)
if err != nil {
return fmt.Errorf("repo: update order status: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("repo: update order status: no order with id %s", id)
}
payload, err := json.Marshal(statusEventPayload{OrderID: id, Status: status})
if err != nil {
return fmt.Errorf("repo: marshal %s payload: %w", eventType, err)
}
if _, err := tx.Exec(ctx, `
insert into outbox (aggregate_id, event_type, payload)
values ($1, $2, $3)`, id, eventType, payload,
); err != nil {
return fmt.Errorf("repo: insert outbox row: %w", err)
}
return tx.Commit(ctx)
}

Save this as services/order/internal/repo/orders.go. Details worth calling out beyond Create’s single transaction:

  • Get and ListByCustomer both call the private itemsForOrder helper rather than duplicating the same select ... from order_items query twice. ListByCustomer does pay for it with an extra query per order (N+1) — acceptable at this course’s scale, and a candidate for a single joined query if this ever became a hot path, exactly the kind of trade-off The Postgres Repository → already called out for a repository this simple.
  • UpdateStatus checks eventType == "" before opening a transaction at all — an invalid status string (anything other than "confirmed" or "cancelled") is rejected immediately, so the function never begins a transaction it would just have to roll back.
  • tag.RowsAffected() == 0 is how UpdateStatus detects “no such order.” UPDATE doesn’t return pgx.ErrNoRows the way a zero-row SELECT does — it succeeds with a command tag reporting how many rows it touched, and zero is the signal to treat as not-found here.
  • Every value in every query is still bound as $1, $2, … — the same SQL-injection-proof discipline as ProductRepo, with zero exceptions for a payload that happens to already be marshaled JSON.
// Package server implements orderv1.OrderServiceServer against an
// OrderRepo and a Catalog gRPC client.
package server
import (
"context"
"errors"
"time"
catalogv1 "github.com/avetavos/shopmicro/gen/shopmicro/catalog/v1"
orderv1 "github.com/avetavos/shopmicro/gen/shopmicro/order/v1"
"github.com/avetavos/shopmicro/services/order/internal/repo"
"github.com/jackc/pgx/v5"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// Server implements orderv1.OrderServiceServer.
type Server struct {
orderv1.UnimplementedOrderServiceServer
repo *repo.OrderRepo
catalog catalogv1.CatalogServiceClient
}
// New returns a Server backed by orderRepo, pricing items via catalog.
func New(orderRepo *repo.OrderRepo, catalog catalogv1.CatalogServiceClient) *Server {
return &Server{repo: orderRepo, catalog: catalog}
}
// CreateOrder prices every requested item against the Catalog service, then
// persists the order.
func (s *Server) CreateOrder(ctx context.Context, req *orderv1.CreateOrderRequest) (*orderv1.Order, error) {
if req.GetCustomerId() == "" {
return nil, status.Error(codes.InvalidArgument, "customer_id is required")
}
if len(req.GetItems()) == 0 {
return nil, status.Error(codes.InvalidArgument, "items must not be empty")
}
priced := make([]repo.PricedItem, 0, len(req.GetItems()))
for _, item := range req.GetItems() {
if item.GetQuantity() <= 0 {
return nil, status.Errorf(codes.InvalidArgument, "quantity for product %s must be positive", item.GetProductId())
}
product, err := s.catalog.GetProduct(ctx, &catalogv1.GetProductRequest{Id: item.GetProductId()})
if err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
return nil, status.Errorf(codes.FailedPrecondition, "product %s does not exist", item.GetProductId())
}
return nil, status.Errorf(codes.Internal, "price product %s: %v", item.GetProductId(), err)
}
priced = append(priced, repo.PricedItem{
ProductID: item.GetProductId(),
Quantity: item.GetQuantity(),
UnitPriceCents: product.GetPriceCents(),
})
}
o, err := s.repo.Create(ctx, req.GetCustomerId(), priced)
if err != nil {
return nil, status.Errorf(codes.Internal, "create order: %v", err)
}
return toProto(*o), nil
}
// GetOrder returns a single order by id.
func (s *Server) GetOrder(ctx context.Context, req *orderv1.GetOrderRequest) (*orderv1.Order, error) {
if req.GetId() == "" {
return nil, status.Error(codes.InvalidArgument, "id is required")
}
o, err := s.repo.Get(ctx, req.GetId())
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, status.Error(codes.NotFound, "order not found")
}
return nil, status.Errorf(codes.Internal, "get order: %v", err)
}
return toProto(*o), nil
}
// ListOrders returns every order placed by a customer.
func (s *Server) ListOrders(ctx context.Context, req *orderv1.ListOrdersRequest) (*orderv1.ListOrdersResponse, error) {
if req.GetCustomerId() == "" {
return nil, status.Error(codes.InvalidArgument, "customer_id is required")
}
orders, err := s.repo.ListByCustomer(ctx, req.GetCustomerId())
if err != nil {
return nil, status.Errorf(codes.Internal, "list orders: %v", err)
}
resp := &orderv1.ListOrdersResponse{}
for _, o := range orders {
resp.Orders = append(resp.Orders, toProto(o))
}
return resp, nil
}
func toProto(o repo.Order) *orderv1.Order {
items := make([]*orderv1.OrderItem, 0, len(o.Items))
for _, it := range o.Items {
items = append(items, &orderv1.OrderItem{
ProductId: it.ProductID,
Quantity: it.Quantity,
UnitPriceCents: it.UnitPriceCents,
})
}
return &orderv1.Order{
Id: o.ID,
CustomerId: o.CustomerID,
Status: toProtoStatus(o.Status),
TotalCents: o.TotalCents,
Items: items,
CreatedAt: o.CreatedAt.Format(time.RFC3339),
}
}
func toProtoStatus(s string) orderv1.OrderStatus {
switch s {
case "pending":
return orderv1.OrderStatus_ORDER_STATUS_PENDING
case "confirmed":
return orderv1.OrderStatus_ORDER_STATUS_CONFIRMED
case "cancelled":
return orderv1.OrderStatus_ORDER_STATUS_CANCELLED
default:
return orderv1.OrderStatus_ORDER_STATUS_UNSPECIFIED
}
}

Save this as services/order/internal/server/server.go. A few mapping and validation details worth calling out:

  • CreateOrder prices items one at a time, in a sequential loop. Each s.catalog.GetProduct call is its own gRPC round trip, so an order with five distinct items makes five sequential calls before the transaction even begins. That’s a real latency cost worth naming honestly — a production version might price items concurrently or batch them into a single Catalog RPC, but neither exists in this course’s .proto contract, so a sequential loop is what’s genuinely correct today.
  • status.FromError(err) unwraps a gRPC error back into its codes.Code and message. This is how CreateOrder tells “Catalog said this product doesn’t exist” (codes.NotFound from Catalog’s own GetProduct) apart from “the call to Catalog itself failed” (a network error, a timeout) — only the first becomes codes.FailedPrecondition; the second becomes codes.Internal, since it isn’t a fact about the product at all.
  • errors.Is(err, pgx.ErrNoRows), exactly like ProductRepo/Server in Catalog. OrderRepo.Get wraps whatever Scan returns with %w, so GetOrder can still unwrap the chain to find the sentinel underneath and translate it into codes.NotFound.
  • toProto’s o.CreatedAt.Format(time.RFC3339) converts the repository’s time.Time into the plain string the .proto contract declared for Order.created_atThe Contracts → chose a string field over google.protobuf.Timestamp for this course, so the mapping is a single Format call rather than a timestamppb conversion.
// Command order runs the Order gRPC server.
package main
import (
"context"
"log"
"net"
"os"
"os/signal"
"syscall"
catalogv1 "github.com/avetavos/shopmicro/gen/shopmicro/catalog/v1"
orderv1 "github.com/avetavos/shopmicro/gen/shopmicro/order/v1"
"github.com/avetavos/shopmicro/pkg/config"
"github.com/avetavos/shopmicro/pkg/pg"
"github.com/avetavos/shopmicro/services/order/internal/repo"
"github.com/avetavos/shopmicro/services/order/internal/server"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/reflection"
)
func main() {
ctx := context.Background()
dbURL := config.Get("ORDER_DB_URL", "postgres://shopmicro:shopmicro@localhost:5432/orders?sslmode=disable")
grpcAddr := config.Get("ORDER_GRPC_ADDR", ":50052")
catalogAddr := config.Get("CATALOG_GRPC_ADDR", ":50051")
pool, err := pg.NewPool(ctx, dbURL)
if err != nil {
log.Fatalf("order: connect to postgres: %v", err)
}
defer pool.Close()
catalogConn, err := grpc.NewClient(catalogAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("order: dial catalog at %s: %v", catalogAddr, err)
}
defer catalogConn.Close()
catalogClient := catalogv1.NewCatalogServiceClient(catalogConn)
lis, err := net.Listen("tcp", grpcAddr)
if err != nil {
log.Fatalf("order: listen on %s: %v", grpcAddr, err)
}
orderRepo := repo.New(pool)
orderServer := server.New(orderRepo, catalogClient)
grpcServer := grpc.NewServer()
orderv1.RegisterOrderServiceServer(grpcServer, orderServer)
reflection.Register(grpcServer)
go func() {
log.Printf("order: gRPC server listening on %s", grpcAddr)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("order: serve: %v", err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
<-stop
log.Println("order: shutting down")
grpcServer.GracefulStop()
}

Save this over services/order/cmd/main.go. The only change from The gRPC Server →‘s version: orderRepo := repo.New(pool) and orderServer := server.New(orderRepo, catalogClient) build the real dependency chain, and RegisterOrderServiceServer registers orderServer instead of &placeholderServer{catalog: catalogClient}. The pool, the listener, the Catalog dial, the reflection registration, the serving goroutine, and the signal handling are all unchanged — none of that was ever about the placeholder.

Run Catalog, then Order:

Terminal window
go run ./services/catalog/cmd
go run ./services/order/cmd

Create a product in Catalog first, so Order has something real to price:

Terminal window
grpcurl -plaintext -d '{"name":"Coffee Mug","description":"350ml ceramic mug","price_cents":1299,"stock":50}' \
localhost:50051 shopmicro.catalog.v1.CatalogService/CreateProduct

Copy the id from the response and place an order for two of them:

Terminal window
grpcurl -plaintext -d '{"customer_id":"cust-1","items":[{"product_id":"8f14e45f-ceea-4c9d-b2a5-0c1e3f4a9b21","quantity":2}]}' \
localhost:50052 shopmicro.order.v1.OrderService/CreateOrder
{
"id": "3a7c9e21-1e4d-4b8a-9c6e-2f8b1d5a7c90",
"customerId": "cust-1",
"status": "ORDER_STATUS_PENDING",
"totalCents": "2598",
"items": [
{
"productId": "8f14e45f-ceea-4c9d-b2a5-0c1e3f4a9b21",
"quantity": 2,
"unitPriceCents": "1299"
}
],
"createdAt": "2026-07-14T09:12:03Z"
}

totalCents is 1299 × 2 = 2598 — priced live from Catalog, never supplied by the client. status comes back ORDER_STATUS_PENDING, matching the 'pending' default the migration wrote. Fetch it back by id:

Terminal window
grpcurl -plaintext -d '{"id":"3a7c9e21-1e4d-4b8a-9c6e-2f8b1d5a7c90"}' \
localhost:50052 shopmicro.order.v1.OrderService/GetOrder

List every order for that customer:

Terminal window
grpcurl -plaintext -d '{"customer_id":"cust-1"}' \
localhost:50052 shopmicro.order.v1.OrderService/ListOrders

Confirm codes.FailedPrecondition for a product that doesn’t exist:

Terminal window
grpcurl -plaintext -d '{"customer_id":"cust-1","items":[{"product_id":"00000000-0000-0000-0000-000000000000","quantity":1}]}' \
localhost:50052 shopmicro.order.v1.OrderService/CreateOrder
ERROR:
Code: FailedPrecondition
Message: product 00000000-0000-0000-0000-000000000000 does not exist

And confirm codes.InvalidArgument for an empty items list:

Terminal window
grpcurl -plaintext -d '{"customer_id":"cust-1","items":[]}' \
localhost:50052 shopmicro.order.v1.OrderService/CreateOrder
ERROR:
Code: InvalidArgument
Message: items must not be empty

Finally, confirm the whole module still builds:

Terminal window
go build ./...

No output means success.

migrations/order/0001_init.sql creates orders, order_items, and outbox — the last with a partial index on unpublished rows that The Transactional Outbox → explains in full. OrderRepo.Create in services/order/internal/repo/orders.go inserts the order, every line item, and an order.created outbox row inside one pgx.Tx, so all three commit together or none do; Get, ListByCustomer, and UpdateStatus round out the repository, the last of these writing its own order.confirmed/order.cancelled outbox row for Order Saga → (Module 9) to eventually trigger. services/order/internal/server/server.go’s Server implements CreateOrder by pricing every item through the injected Catalog client — codes.FailedPrecondition for a product Catalog reports missing, codes.Internal for any other Catalog failure — before ever calling repo.Create; GetOrder and ListOrders round-trip straight to the repository. grpcurl confirms the whole path end-to-end: create a product in Catalog, place an order in Order priced from that product’s real price_cents, fetch it back, list it, and see both codes.FailedPrecondition and codes.InvalidArgument fire correctly. That’s the Order service placing real, correctly-priced orders. Next, The Transactional Outbox → explains exactly why that outbox insert — and not a direct publish to Kafka — is what makes order.created reliable.