The codebase follows DDD with:
- Entities - Objects with identity (Todo, User, Role, Permission)
- Value Objects - Immutable objects without identity (TodoFilters)
- Aggregate Roots - Consistency boundaries (Todo)
- Domain Services - Business logic that doesn't belong to a single entity
- Repository Interfaces - Abstractions for data access
- Specifications - Reusable business rules
- Domain Events - Notifications of state changes
Dependency direction is strictly enforced:
Interface Layer → Application Layer → Domain Layer ← Infrastructure Layer
- Domain has zero external dependencies
- Application depends only on Domain
- Infrastructure implements Domain interfaces
- Interface depends on Application
- Commands mutate state (Create, Update, Delete, Complete)
- Queries read state (Get, List, Search)
- Handlers are separated into
command/andquery/packages
Each feature is a vertical slice:
internal/todo/
├── domain/ # Business rules
├── application/ # Use cases
├── infrastructure/ # External concerns
└── interfaces/ # Delivery mechanisms
Each module is independent:
- Owns its own database tables
- Communicates through interfaces or domain events
- Registers itself via
fx.Module - Can be extracted to a microservice
All infrastructure implementations are behind interfaces defined in internal/core/domain/:
Cacheinterface →internal/infrastructure/cache/(Redis)Messengerinterface →internal/infrastructure/messaging/(NATS)TokenServiceinterface →internal/infrastructure/auth/(JWT)Loggerinterface →internal/infrastructure/logger/(Zap)
To swap a technology, implement the interface and update the fx.Module binding.
Example CRUD module demonstrating the full architecture pattern:
- Domain entity, repository interface, domain service
- CQRS command/query handlers
- SQLC persistence, Redis cache
- HTTP handlers with Swagger annotations
JWT-based authentication with access and refresh tokens:
- User entity with bcrypt password hashing
- Register, Login, Refresh, Logout endpoints
- Refresh tokens stored in DB (opaque, single-use)
- Access tokens are short-lived JWTs (15 min)
Casbin-based RBAC with database-backed policies:
- Role and Permission entities
- User-Role and Role-Permission assignments
- Casbin enforcer loads policies from DB on startup
- Middleware enforces permissions on protected routes
- Check permission endpoint for runtime validation
Services publish domain events to an EventBus interface (in-memory synchronous by default, swappable for RabbitMQ/Kafka). An EmailHandler subscribes to auth domain events (auth.user.registered, auth.user.email_verified, auth.user.password_reset_requested) and calls the appropriate mailer method.
Flow: Service → EventBus.Publish() → EmailHandler → domain.Emailer.Send*()
A LoggingEventBus decorator wraps the concrete bus and logs every publish failure through domain.Logger. Event handlers return errors instead of swallowing them, so mailer failures (SMTP down, SendGrid error, etc.) are recorded without breaking the originating HTTP request. Services discard the returned publish error after the decorator has logged it, keeping side effects best-effort.
Every HTTP request gets an OpenTelemetry span via a Chi middleware. The middleware:
- Extracts incoming W3C trace context (
traceparent/baggageheaders) - Starts a span named
<METHOD> <path> - Records the response status code
- Marks the span as error for 4xx/5xx responses
The ZapLogger extracts trace_id and span_id from the context and attaches them to every log entry, so logs and traces are correlated. Panics and 5xx errors are recorded as exceptions on the current span.
All HTTP responses use a unified envelope:
// Success (single)
{"success": true, "data": {...}, "meta": null}
// Success (paginated list)
{"success": true, "data": [...], "meta": {"page": 1, "per_page": 20, "total": 100, "total_pages": 5}}
// Error
{"success": false, "data": null, "error": {"code": "VALIDATION_ERROR", "message": "..."}}Errors are mapped to HTTP status codes via utils.MapError, which translates domain.ErrNotFound, domain.ErrConflict, etc.
Every module follows this pattern:
var Module = fx.Module("modulename",
fx.Provide(/* ... */),
fx.Invoke(/* ... */),
)The main application composes modules:
app := fx.New(
fx.Supply(cfg),
logger.Module,
cache.Module,
auth.Module,
messaging.Module,
database.Module,
telemetry.Module,
authentication.Module,
authorization.Module,
todo.Module,
)- Create
internal/newmodule/with domain, application, infrastructure, interfaces - Define domain entities and repository interfaces
- Implement application services and handlers
- Implement infrastructure (persistence, cache)
- Implement interface (HTTP, gRPC, etc.)
- Create
module.gowithfx.Module - Add to
main.go