Skip to content

shopnexus/shopnexus-server

Repository files navigation

ShopNexus Server

wakatime

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

Architecture

Entry point: cmd/server/main.gofx.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).

Module Structure

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

Request Flow

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"]
Loading

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
Loading

Restate Durable Execution

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.

Integration Pattern

  1. interface.go defines the XxxBiz interface, XxxHandler struct, ServiceName() method, and a //go:generate directive.
  2. restate_gen.go is auto-generated by cmd/genrestate/ — implements XxxBiz via HTTP calls to Restate ingress.
  3. fx.go provides both *XxxHandler (for Restate registration) and XxxBiz (proxy, for cross-module deps and transport).
  4. Transport handlers depend on XxxBiz (interface), never *XxxHandler (concrete).
  5. app/restate.go registers all Handler structs with restate.Reflect() and auto-registers with the Restate admin API on startup.

Database Design

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. Uses guregu/null/v6 for nullable types.
  • pgtempl (cmd/pgtempl/) generates SQLC query templates from migration files, producing CRUD queries automatically. Custom queries go in *_custom.sql files (not overwritten by pgtempl).

Nullable Types

  • guregu/null/v6 for DB fields in SQLC-generated structs.
  • *T pointers for JSON serialization in models exposed to Restate — generic types like null.Value[T] break Restate's JSON schema generation.
  • ptrutil.PtrIf[T](val, valid) for converting nullable DB types to pointers.

Code Conventions

Naming

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

Error Handling

  • Client-visible errors: sharedmodel.NewError(httpStatusCode, "message") defined in each module's model/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 of fmt.Errorf — it preserves the terminal flag through the error chain.

Code Generation Pipeline

Three generators run in sequence:

  1. pgtempl → generates SQLC query templates from migration SQL
  2. SQLC → generates type-safe Go from those query templates
  3. genrestate → generates Restate proxy clients from XxxBiz interfaces

Infrastructure

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

External Providers

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

Modules

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

About

ShopNexus is a microservice e-commerce platform

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages