Skip to content

Repository files navigation

Product Catalog Service

A simplified Product Catalog Service built with DDD, Clean Architecture, CQRS, gRPC, and Google Cloud Spanner.

Prerequisites

  • Go 1.21+
  • Docker & Docker Compose
  • protoc (Protocol Buffers compiler)
  • protoc-gen-go, protoc-gen-go-grpc

Quick Start

# Start Spanner emulator
docker-compose up -d

# Run migrations (creates instance, database, and schema)
make migrate

# Run tests
make test

# Start gRPC server (port 50051)
make run

Architecture

Layers

Transport (gRPC) → Use Cases (Commands/Queries) → Domain → Repository → Spanner
  • Domain Layer (internal/app/product/domain/): Pure Go business logic. No external dependencies — no context.Context, no database imports, no proto imports. Contains the Product aggregate, Money/Discount value objects, domain events, and sentinel errors.

  • Use Cases (internal/app/product/usecases/): Commands that follow the Golden Mutation Pattern: Load aggregate → Execute domain logic → Build commit plan → Apply atomically.

  • Queries (internal/app/product/queries/): Read-side handlers that bypass the domain for optimized reads via the read model interface.

  • Repository (internal/app/product/repo/): Spanner implementation. Repositories return *spanner.Mutation — they never apply mutations themselves. The UpdateMut method reads the change tracker to build targeted updates (only dirty fields).

  • Transport (internal/transport/grpc/product/): Thin gRPC handlers that validate requests, map to use case inputs, call the use case, and map domain errors to gRPC status codes.

Key Patterns

  • Golden Mutation Pattern: Every write operation: Load → Domain logic → Build plan (mutations) → Apply plan atomically
  • CQRS: Commands go through the domain aggregate; queries use a read model with DTOs
  • Transactional Outbox: Domain events are stored in the outbox_events table within the same Spanner transaction as the aggregate mutation
  • Change Tracking: The ChangeTracker on the Product aggregate tracks which fields were modified, enabling targeted Spanner updates
  • CommitPlan: A local wrapper around spanner.Client.Apply() that batches mutations (replaces the private github.com/Vektor-AI/commitplan library)

Money Handling

Monetary values use *big.Rat (rational numbers) for precise decimal arithmetic. Stored in Spanner as a numerator/denominator INT64 pair.

price := domain.NewMoney(1999, 100) // $19.99

Design Decisions & Trade-offs

  1. commitplan wrapper: The required github.com/Vektor-AI/commitplan library is private. Implemented a minimal compatible wrapper (internal/pkg/committer/) that batches *spanner.Mutation and applies them via spanner.Client.Apply().

  2. Product starts as draft: New products must be explicitly activated before discounts can be applied. This enforces a clear lifecycle: draft → active → inactive → archived.

  3. One discount per product: Only one active discount at a time per the requirements. Applying a new discount replaces the existing one.

  4. Offset-based pagination: Used for ListProducts with limit/offset. Simpler than cursor-based pagination and sufficient for the use case.

  5. FakeClock for testing: Time is injected via a Clock interface, allowing deterministic E2E tests without real time dependencies.

  6. Outbox events are stored, not published: Per requirements, the outbox pattern stores events transactionally. A separate processor (not implemented) would publish them.

Project Structure

├── cmd/server/main.go              # Entry point, migrations, gRPC server
├── internal/
│   ├── app/product/
│   │   ├── domain/                 # Pure domain (aggregate, value objects, events, errors)
│   │   │   └── services/           # Domain services (pricing calculator)
│   │   ├── usecases/               # Command interactors (create, update, activate, etc.)
│   │   ├── queries/                # Query handlers (get, list)
│   │   ├── contracts/              # Repository & read model interfaces
│   │   └── repo/                   # Spanner implementations
│   ├── models/                     # DB model structs & mutation helpers
│   ├── transport/grpc/product/     # gRPC handlers, mappers, error mapping
│   ├── services/                   # DI container
│   └── pkg/                        # Shared utilities (committer, clock)
├── proto/product/v1/               # Proto definition & generated code
├── migrations/                     # Spanner DDL
├── tests/e2e/                      # End-to-end tests against Spanner emulator
├── docker-compose.yml              # Spanner emulator
└── Makefile

Testing

Unit tests cover domain logic in isolation (55 tests):

go test ./internal/... -v

E2E tests run against the Spanner emulator:

docker-compose up -d
SPANNER_EMULATOR_HOST=localhost:9010 go test ./tests/e2e/ -v -count=1

E2E tests cover: product creation, updates, activation/deactivation, discount application/removal, business rule validation, and outbox event verification.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages