A social marketplace backend in Go, built as a modular monolith designed for microservice extraction via Restate durable execution.
No customer/vendor distinction — any account can both buy and sell. Orders track buyer_id and seller_id per transaction.
Development timeline: timeline.md
Entry point: cmd/server/main.go → fx.New(app.Module).Run().
The server is a modular monolith — eight vertical-slice modules that each own their database schema, business logic, and HTTP transport. Modules communicate through Restate durable execution, meaning every cross-module call is an HTTP request to the Restate ingress. This gives us exactly-once delivery, automatic retries, and a clear extraction path to microservices — any module can be deployed as a standalone service by pointing its Restate registration to a different host.
Dependency injection is handled by Uber fx. Each module's fx.go provides both the concrete *XxxHandler (registered with Restate) and the XxxBiz interface (a generated Restate proxy used by other modules and transport handlers).
Every module under internal/module/<name>/ follows the same vertical-slice layout:
biz/
interface.go # XxxBiz interface + XxxHandler struct + constructor + go:generate directive
restate_gen.go # Auto-generated Restate HTTP client (DO NOT EDIT)
*.go # Business logic methods (use restate.Context)
db/
migrations/ # SQL schema (*.up.sql / *.down.sql)
queries/ # SQLC query templates (pgtempl-generated + *_custom.sql for hand-written)
sqlc/ # Generated DB code (DO NOT EDIT)
model/
*.go # DTOs, domain models, error sentinels
transport/echo/
*.go # HTTP handlers
fx.go # Uber fx module wiring
flowchart LR
Client([Client]) -->|HTTP| Echo["Echo Handler"]
Echo -->|depends on| Biz["XxxBiz\n(interface)"]
Biz -.->|"fx resolves to"| Proxy["XxxRestateClient\n(generated)"]
Proxy -->|HTTP| Restate["Restate\nIngress"]
Restate -->|routes to| Handler["XxxHandler\n(restate.Context)"]
Handler -->|"restate.Run()"| SQLC["SQLC → PostgreSQL"]
Cross-module calls follow the same path — when OrderHandler needs account data, it calls AccountBiz (interface), which fx resolves to AccountRestateClient, which goes through Restate ingress to AccountHandler:
flowchart LR
OrderHandler -->|"calls AccountBiz"| AccountBiz["AccountBiz\n(interface)"]
AccountBiz -.->|proxy| AccountRestate["AccountRestateClient"]
AccountRestate -->|HTTP| Ingress["Restate\nIngress"]
Ingress -->|routes to| AccountHandler
All business logic methods use restate.Context instead of context.Context. This is required for Restate's Reflect() registration and enables:
- Durable side effects: DB writes inside
restate.Run()closures are journaled and replay-safe. If the process crashes mid-execution, Restate replays the journal and skips already-completed steps. - Cross-module RPC: calls between modules go through auto-generated proxy clients (
XxxRestateClient), which are HTTP calls to the Restate ingress. This makes every cross-module call durable and retryable. - Fire-and-forget:
restate.ServiceSend(ctx, "ServiceName", "MethodName").Send(params)for asynchronous work like notifications and analytics tracking — durable, exactly-once delivery. - Terminal errors: client-facing errors (validation, not found, conflict) use
.Terminal()to prevent Restate from retrying them.
interface.godefines theXxxBizinterface,XxxHandlerstruct,ServiceName()method, and a//go:generatedirective.restate_gen.gois auto-generated bycmd/genrestate/— implementsXxxBizvia HTTP calls to Restate ingress.fx.goprovides both*XxxHandler(for Restate registration) andXxxBiz(proxy, for cross-module deps and transport).- Transport handlers depend on
XxxBiz(interface), never*XxxHandler(concrete). app/restate.goregisters all Handler structs withrestate.Reflect()and auto-registers with the Restate admin API on startup.
Each module owns a PostgreSQL schema (account.*, catalog.*, order.*, etc.) — no cross-schema foreign keys. This enforces module boundaries at the database level and makes future extraction straightforward.
The database layer uses three tools:
- pgx/v5 as the PostgreSQL driver, wrapped in
pgsqlc.Storage[T]for connection pooling and transaction support. - SQLC generates type-safe Go structs and query methods from SQL. Config in
sqlc.yaml. Usesguregu/null/v6for nullable types. - pgtempl (
cmd/pgtempl/) generates SQLC query templates from migration files, producing CRUD queries automatically. Custom queries go in*_custom.sqlfiles (not overwritten by pgtempl).
guregu/null/v6for DB fields in SQLC-generated structs.*Tpointers for JSON serialization in models exposed to Restate — generic types likenull.Value[T]break Restate's JSON schema generation.ptrutil.PtrIf[T](val, valid)for converting nullable DB types to pointers.
| Thing | Pattern | Example |
|---|---|---|
| Interface (public API) | XxxBiz |
AccountBiz |
| Implementation | XxxHandler |
AccountHandler |
| Generated Restate proxy | XxxRestateClient |
AccountRestateClient |
| Constructor | NewXxxHandler |
NewAccountHandler |
| Import alias for shared model | sharedmodel |
internal/shared/model |
- Client-visible errors:
sharedmodel.NewError(httpStatusCode, "message")defined in each module'smodel/error.go. Always return with.Terminal()to prevent Restate retries. - Internal errors:
fmt.Errorf("action: %w", err)— no "failed to" prefix (e.g.,fmt.Errorf("get account: %w", err)). - Wrapping terminal errors: use
sharedmodel.WrapErr(msg, err)instead offmt.Errorf— it preserves the terminal flag through the error chain.
Three generators run in sequence:
- pgtempl → generates SQLC query templates from migration SQL
- SQLC → generates type-safe Go from those query templates
- genrestate → generates Restate proxy clients from
XxxBizinterfaces
| Service | Package | Purpose |
|---|---|---|
| PostgreSQL | internal/shared/pgsqlc |
Multi-schema DB with pgxpool |
| Redis | internal/infras/cache |
Struct caching (Sonic JSON serialization) |
| NATS | internal/infras/pubsub |
Message queue (JetStream) |
| Restate | internal/infras/restate |
Durable execution HTTP ingress |
| Milvus | internal/infras/milvus |
Vector search for hybrid product search |
| S3/Local | internal/infras/objectstore |
File storage with presigned URLs |
Payment and transport are pluggable via a map[string]Client pattern. Each provider implements a Client interface and is registered at startup.
| Provider | Package | Implementations |
|---|---|---|
| Payment | internal/provider/payment |
VNPay (QR/Bank/ATM), COD |
| Transport | internal/provider/transport |
GHTK (Express/Standard/Economy) |
| Geocoding | internal/provider/geocoding |
Nominatim (OpenStreetMap) |
| LLM | internal/provider/llm |
OpenAI, AWS Bedrock, Python backend |
Each module has its own README with ER diagrams, domain concepts, flows, and endpoints.
| Module | Description |
|---|---|
account |
Auth, profiles, contacts, favorites, payment methods, notifications |
catalog |
Products (SPU/SKU), categories, tags, comments, hybrid search, recommendations |
order |
Cart, checkout, pending items, seller confirmation, payment, refunds |
inventory |
Stock management, serial tracking, audit history |
promotion |
Discounts, ship discounts, scheduling, group-based price stacking |
analytic |
Interaction tracking, weighted product popularity scoring |
chat |
REST messaging, conversations, read receipts |
common |
Resource/file management, object storage, service options, SSE, geocoding |