source-base is a production-ready microservice template built on Hexagonal Architecture (Clean Architecture).
Goals:
- Fully decouple business logic from frameworks and infrastructure
- Easy to extend, test, and maintain
- Use modern, high-performance Go libraries
| Dependency | Version | Purpose |
|---|---|---|
entgo.io/ent |
v0.14.6 |
ORM code generation |
github.com/labstack/echo/v5 |
v5.2.1 |
HTTP framework |
github.com/google/wire |
v0.7.0 |
Dependency injection |
github.com/redis/go-redis/v9 |
v9.21.0 |
Redis client |
go.uber.org/zap |
v1.28.0 |
Structured logging |
github.com/go-playground/validator/v10 |
v10.30.3 |
Request validation |
github.com/google/uuid |
v1.6.0 |
UUID generation (V7) |
github.com/jackc/pgx/v5 |
v5.10.0 |
PostgreSQL driver |
github.com/natefinch/lumberjack |
v2.0.0 |
Log rotation |
ariga.io/atlas |
v0.36.2-... |
Schema migration |
gopkg.in/yaml.v3 |
v3.0.1 |
YAML config parsing (indirect) |
github.com/golang-jwt/jwt/v5 |
v5.3.1 |
JWT (indirect) |
Language: Go 1.26
source-base/
├── cmd/server/ # Entry point (main.go) — load config, DI init, start server
├── configs/ # Sample YAML config files
├── internal/
│ ├── bootstrap/ # Initialize components (Postgres, Redis, Logger, Router, TxManager, OTel)
│ ├── config/ # Config struct definition, read from YAML
│ ├── const/ # Context keys using custom type (TxKey)
│ ├── controller/http/ # HTTP controllers — receive request, call service via interface
│ ├── di/ # Dependency Injection with Google Wire
│ ├── domain/
│ │ ├── dto/ # Data Transfer Objects (request/response) — pure Go
│ │ └── entity/ # Domain entities — pure Go, zero dependency
│ ├── ent/
│ │ ├── schema/ # Ent schema definitions (User, Pet) + mixins
│ │ ├── generate/ # Auto-generated code by Ent ORM
│ │ ├── migrate/ # Migration CLI using Atlas
│ │ ├── migrations/ # Generated SQL migration files
│ │ ├── repository/ | Implement ports repository (using Ent client)
│ │ ├── generate.go # go:generate directive
│ │ └── mapper.go # Map Ent model → Domain entity
│ ├── ports/ # Interface definitions (hexagonal ports)
│ └── service/ # Business logic layer
├── logs/ # Log files output
├── pkg/
│ ├── common/
│ │ ├── apperror/ # AppError type (Code, Message, RootCause, HTTPStatus)
│ │ ├── cache/ # Cache abstraction interfaces (Engine, SortedSetEngine)
│ │ ├── http/ # HTTP utilities: handler Wrap, ErrorHandler, middlewares,
│ │ │ # request Parse, response format, validation
│ │ ├── tx/ # Transaction Manager interface
│ │ └── workerpool/ # Worker pool (not yet used — TODO)
│ ├── constraints/ # Go generics type constraints + context keys
│ ├── database/
│ │ ├── ent/ # PostgreSQL driver, generic TxManager, mixins (UUID, PublicID, Time)
│ │ └── redis/ # Redis client implementing cache.Engine + SortedSetEngine
│ ├── dto/ # Shared DTOs (ZMember for Redis sorted sets)
│ ├── logger/ # Logger initialization (zap + lumberjack)
│ ├── settings/ # Standard config structs
│ ├── unique/ # Unique ID generation (public_id prefix+date+base62, RandBase62)
│ └── utils/ # Utilities (time, token, converter)
└── storages/logs/ # Log storage
Controller (HTTP) ──calls──→ Service (business logic) ──calls──→ Repository (data)
- Controller receives HTTP request → parse & validate → calls
ports.UserService→ returns standardized response - Service receives DTO request → maps to domain entity → calls
ports.UserRepository→ maps result to DTO response - Repository uses Ent ORM to interact with PostgreSQL, maps Ent model → domain entity
Domain Entity / DTO ←── no dependencies (pure Go)
Ports ──uses──→ DTO + Entity (data types only)
Service ──calls──→ Ports interface + DTO + Entity
Controller ──calls──→ Ports interface + DTO
Repository ──implements──→ Ports interface + Entity + Ent generate
pkg/ ── does not import internal/* ──
- domain/entity and domain/dto are pure Go structs with zero imports from any framework (not Ent, Echo, or database drivers)
- internal/ent/mapper.go handles mapping from Ent model (
generate.User) to domain entity (entity.User), keeping the rest of the application independent of Ent ORM - This allows swapping ORM or database without affecting business logic
Transactions are managed via a context-based pattern:
pkg/database/ent/tx.go: genericWithTx[T]()performs begin → fn → commit/rollback, with panic recoverypkg/database/ent/tx.go:txManager[T]implementstx.Managerinterface, uses generics to inject transaction into contextinternal/bootstrap/tx.go: wiresclient.Txwith context injection function (context.WithValue(ctx, TxKey, tx))internal/const/key.go:TxKeyuses custom typecontextKey(not a plain string) to avoid collisioninternal/ent/repository/tx.go:GetClient(ctx, client)checks context — if*generate.Txexists in context, returnstx.Client(), otherwise returns the original client
When a service needs to run inside a transaction:
txManager.DoInTx(ctx, func(txCtx context.Context) error {
// All repository calls here automatically use the tx client
return nil
})Every repository method calls GetClient(ctx, u.client) at the start, so they automatically follow the transaction if one is present.
| Tool | Version | Notes |
|---|---|---|
| Go | 1.26+ | Per go.mod |
| Docker | latest | Run Postgres, Redis locally |
| Atlas CLI | >= 0.14 | Install: go install ariga.io/atlas/cmd/atlas@latest |
git clone <repo-url> source-base
cd source-base
# .env file is already included with dev defaultsdocker compose up -dThis starts:
postgres-main(PostgreSQL 15, port 5432, database: app_db)redis(Redis 7, port 6379)adminer(DB admin panel, port 3001)postgres-atlas(additional PostgreSQL for Atlas dev, port 5433 — fromdocker-compose.override.yaml)
make db-upmake run
# or
go run cmd/server/main.goServer runs at http://localhost:8080.
Edit schema (internal/ent/schema/*.go)
→ go generate (make ent-gen)
→ Generate migration file (make db-diff name=<name>)
→ Review SQL in internal/ent/migrations/
→ Apply (make db-up)
| Command | Description |
|---|---|
make ent-new name=<Entity> |
Create new schema |
make ent-desc |
Display current schema structure |
make ent-gen |
Generate Ent ORM code (client, CRUD builders, predicates) |
make db-diff name=<name> |
Diff schema vs DB → generate migration file |
make db-up |
Apply migrations to database |
make db-down |
Rollback migrations |
make db-hash |
Rehash migration files |
make db-status |
Check migration status |
Note: db-diff uses docker-compose.override.yaml — the postgres-atlas container (port 5433) must be running to serve as the dev database for Atlas.
| Method | Path | Description |
|---|---|---|
GET |
/ping |
Health check |
POST |
/api/v1/public/users |
Create a new user |
GET |
/api/v1/public/users/:public_id |
Get user by public_id (with pets list) |
Sample Request/Response:
POST /api/v1/public/users
{
"name": "Nguyen Van A",
"email": "a@example.com"
}Standard response format:
{
"code": 20000,
"message": "Success",
"data": {
"public_id": "UR20260705xK9",
"name": "Nguyen Van A",
"email": "a@example.com",
"created_at": "2026-07-05T12:00:00Z",
"updated_at": "2026-07-05T12:00:00Z"
}
}| Code | Meaning |
|---|---|
| 20000 | Success |
| 20001 | Created |
| 40000 | Invalid parameters |
| 40001 | Validation failed |
| 44000 | Resource not found |
| 50000 | Internal server error |
| 50001 | Database error |
Full details at pkg/common/http/response/codes.go.
No tests yet. The project is currently under development, with no *_test.go files present.
When tests are added, run them with:
go test ./...Key conventions extracted from the actual codebase:
- Domain entity does not import Ent:
internal/domain/entity/*.goare pure Go structs with no framework imports (no Ent, Echo). Only usestime.Timeanduuid.UUID. - Repository returns
(*entity.T, error): Does not mutate input parameters. Exception:Create(ctx, *entity.User)reassigns*user = *createdto return the ID to caller. Other methods (GetByPublicID) return a new pointer. - Explicit Set().Set() when creating Ent records: No reflection or generic mapping. Each field is explicitly set:
client.User.Create().SetName(user.Name).SetEmail(user.Email).Save(ctx). - Context key uses custom type:
internal/const/key.gousestype contextKey stringinstead of a plain string to prevent collisions between packages. - Transaction uses generics:
pkg/database/ent/tx.gouses genericWithTx[T Tx]()— type-safe, no interface casting needed. - Generic HTTP handler:
pkg/common/http/handler/wrapper.gousesWrap[RQ, RS any](Func[RQ, RS])— parses request, calls handler, returns standardized response — controller only needs to implementfunc(context.Context, *RQ) (RS, error). - Validation messages in English:
pkg/common/http/validation/validator.gouses json tag names and English messages ("name is required","email must be a valid email"). pkg/does not importinternal/*: Packages inpkg/are designed to be reusable and independent ofinternal/(exceptpkg/common/txused by internal).
TODO: No license information yet. Currently under internal development.