The codebase is DDD-inspired (Domain-Driven Design) while pragmatic: it enforces a clear separation between domain logic and infrastructure concerns, keeps domain types and invariants explicit, and avoids excessive boilerplate. The design prioritizes testability, clarity, and maintainability, making it easy for another developer to read and reason about, while being practical for production.
- Single-binary Go service with minimal external dependencies
- Clear boundaries: domain, services (use-cases), repositories (infrastructure), and transport (HTTP).
- Strong domain modeling: entities, value objects, rehydration, and domain errors.
- Production-minded choices: cursor-based pagination, connection pooling via pgx, JWT auth, request timeouts.
flowchart LR
Client["External Client"] -->|HTTP| HTTP["Transport Layer (HTTP API)"]
HTTP --> Handlers["Handlers (request validation, DTOs)"]
Handlers --> Services["Application Services (Use-cases, Orchestration)"]
Services --> Domain["Domain Entities & Value Objects"]
Services --> Repos["Repository Interfaces"]
Repos --> Postgres["Postgres (db) - db package"]
cmd/api/main.go # entrypoint: load config, connect DB, migrate, start server
internal/app # dependency wiring (repositories -> services -> handlers -> router) and build router
internal/transport/http # HTTP handlers, router, request/response DTOs, middleware
internal/<domain> # device, command, telemetry, token, user—domain models, services, errors
internal/db # repositories and migrations
internal/config, logger # support utilitiesThe domain layer defines the core Entities, Value Objects, and Invariants in internal/<domain> (e.g., internal/command/command.go). This layer encapsulates the business rules and domain logic, ensuring they are reusable, consistent, and independent of infrastructure.
Key principles:
-
Entities represent objects with a persistent identity, such as
CommandorDevice. -
Value Objects represent immutable concepts defined solely by their attributes, such as
Name,Status,ExecutedAt, orPayload. -
Invariants enforce rules and constraints that must always hold, such as:
- Command names must be 3–50 characters and match a specific regex.
- Payloads must be non-empty JSON objects.
ExecutedAttimestamps cannot be in the future.
Domain objects provide constructors and methods to:
- Validate and enforce invariants at creation.
- Update their state in a controlled way (e.g.,
UpdateStatus,UpdateExecutedAt). - Rehydrate from persistent storage safely, ensuring any corrupted data is detected and rejected.
Application Services (or use-case services) orchestrate interactions between the domain layer and the infrastructure layer to perform domain operations. They are responsible for executing business use-cases without containing domain rules themselves.
Key responsibilities:
- Create and manipulate domain objects using constructors and methods provided by the domain layer.
- Persist domain objects or query data via repositories.
- Accept and return DTOs at the transport layer boundary, while internal operations work directly with domain objects.
- Application Services do not contain business rules—those belong in the domain layer.
- They orchestrate the steps needed to complete a use-case: creation, persistence, and updating of domain objects.
- They bridge DTOs from transport layers to domain objects, ensuring the domain remains isolated and consistent.
Here’s a polished and structured version of your Transport Layer (HTTP Handlers) section for system-design.md that’s clearer and consistent with your previous sections:
The transport layer is responsible for translating external input (e.g., JSON requests) into domain-level value objects, invoking application services, and returning results as JSON responses. Handlers also perform request validation, error handling, and middleware integration.
- Input validation: Ensure incoming data conforms to required formats before creating domain objects.
- DTO mapping: Convert JSON requests into domain value objects and domain objects into JSON responses.
- Orchestration: Call application services to execute business use-cases.
- Error handling: Return appropriate HTTP status codes and messages for client or server errors.
- Middleware integration: Support authentication, logging, and metrics collection.
-
Domain-specific errors are exposed (e.g., command.ErrCommandNotFound, device.ErrDeviceNotFound) and mapped to appropriate HTTP status codes in handlers
-
DB constraint errors are inspected and translated (foreign key constraint → ErrDeviceNotFound).
- Pragmatic DDD: explicit domain types and rehydration give strong invariants and fewer runtime surprises. Avoided heavy frameworks to keep codebase simple and easy for new contributors.
- Cursor-based pagination vs offset:
- Pros: consistent, efficient, scales for large datasets.
- Cons: cursor management complexity for clients (but encoded cursor string hides internals)