6.7 KiB
6.7 KiB
Repository Guidelines
This is a Kratos service template. This file owns the layering contract agents must follow when changing the template.
Project structure
api/<domain>/<version>/ Proto sources and generated stubs. Public contract.
cmd/<app>/ Entrypoint, Wire injector, main.go.
configs/ Runtime config (config.yaml). No secrets.
internal/config/ Viper config models, loading, snapshots, and reloads.
internal/global/ Process-wide shared resource registry.
internal/server/ HTTP/gRPC server wiring.
internal/service/ Transport adapters; one file per resource.
internal/biz/ Domain models, usecases, repo interfaces, errors.
internal/data/ Repo implementations, database clients, migrations.
internal/initialize/ First-install and configuration orchestration.
internal/integration/ External I/O adapters: cache, email, payment, storage.
internal/logging/ Application logging infrastructure.
internal/security/ Security mechanisms such as admin JWT handling.
internal/utils/ Stateless, internal-only helper packages.
Layering & dependency rules
Three model shapes flow through three layers. biz owns the DO, data
owns the PO; service is a pass-through that converts at its boundary.
client ──► DTO ──► service ──► DO ──► biz ──► DO ──► data ──► PO ──► storage
▲ ▲
│ declares │ implements
└─── repo IF ────┘
DTO Data Transfer Object — proto request / response.
DO Domain Object — pure biz model, no proto, no storage tags.
PO Persistent Object — storage shape, owned by `data`.
| Layer | Owns | Speaks at boundary | Never speaks |
|---|---|---|---|
| service | — | DTO ↔ DO | PO, storage client |
| biz | DO | DO | DTO, PO, storage client |
| data | PO | DO ↔ PO | DTO |
serviceimportsapi/...(DTO) andbiz(DO). Neverdata.bizimportsapi/...only for error reason enums. Neverservice, neverdata. The repo interface declared here is the inversion seam.dataimportsbizto implement the repo interface. Neverservice, never DTOs.integrationimplements external I/O boundaries and may importbizand provider SDKs. It is not a utility layer.utilscontains stateless helpers only. It must not own clients, watchers, repositories, runtime configuration, or provider SDK lifecycles.cmdis the only place that wires all layers via Wire.
A change crossing these arrows the wrong way is a layering bug; fix the design rather than add the import.
Layer responsibilities
service (DTO ↔ DO)
convert<Resource>parses an incoming proto into a DO. The reverse direction is built inline at the return site; the reply type is whatever the proto declares — usually the resource itself (return &v1.<Resource>{...}, nil), sometimes a list wrapper (*v1.<Resources>Set), or&emptypb.Empty{}for deletes. Inlining keeps each handler self-contained.- Embed
Unimplemented<Resource>ServiceServer. - Parse AIP list requests via
filtering/ordering/pagination; applyfieldmask.Updatefor partial updates. - Validate request inputs at the service boundary before delegating to the usecase.
- Return
bizerrors. No business rules, no storage access, no PO.
biz (DO only)
- Owns the DO (
type <Resource> struct— no proto, no storage tags), the usecase, and the repo interface (type <Resource>Repo interface). - Owns typed errors built with
errors.NotFound/errors.BadRequestplus the API error reason enum. - Owns
ListOptionhelpers —ListFilter,ListOrderBy,ListOffset,ListLimit— so callers compose queries without leaking storage primitives.
data (DO ↔ PO)
- Repo shape: implement
biz.<Resource>Repo. The constructor returns the interface, never the concrete type:func New<Resource>Repo(d *Data) biz.<Resource>Repo. - PO and conversion: define a PO when the storage shape diverges from
the DO. PO types stay inside
data. Use free functionsnew<Resource>(DO → PO, write) andtoBiz(PO → DO, read). Driver-specific builder types never leavedata. - Shared clients:
*Data(declared ininternal/data/data.go) holds long-lived storage clients. Repos receive*Dataand never construct their own clients. - Querying: translate
ListOptions.FilterandListOptions.OrderByinto the storage driver's query language inside the repo. - Errors: map driver errors to
biztyped errors so callers above never branch on the driver.
server
- Construct HTTP/gRPC servers, apply middleware, register services. No translation, no business logic.
Add-a-resource checklist
- DTO: define
Create<Resource>/Get<Resource>/List<Resources>/Update<Resource>/Delete<Resource>inapi/<domain>/<version>/, thenmake api. - DO + repo interface: declare both in
biz; build the usecase on top of the interface. - Repo impl: implement in
datareturningbiz.<Resource>Repo; add a PO and the matching conversion helpers when storage shape diverges from DO. - Wiring: register the repo constructor in
data.ProviderSet, the usecase inbiz.ProviderSet, the service inservice.ProviderSet; register HTTP/gRPC services ininternal/server. - Regenerate:
make allto refresh Wire andgo.mod.
Testing seam
Tests live beside the code they cover (*_test.go). Test layers in
isolation: service tests fake the usecase, biz tests fake the repo, data
tests exercise repo implementations at the storage boundary.
Generation & generated files
Regenerate via make api or make all; never hand-edit
*.pb.go, *_grpc.pb.go, *_http.pb.go, or wire_gen.go.
Naming & error reasons
- Resource:
<Resource>(e.g.,Todo); collection RPC:List<Resources>. - Types: repo
<Resource>Repo, usecase<Resource>Usecase, service<Resource>Service. PO types live insideinternal/data/; pick a name that fits the storage driver and convert withnew<Resource>(do)/toBiz(po)free functions. - Error reasons: declared in
api/<domain>/<version>/error_reason.proto, surfaced asErr<Resource><Cause>inbiz.
Commits & security
- Conventional Commits:
feat:,fix:,refactor:,chore(deps):,docs:,test:. Regenerated files belong in the same commit as their source. - Never commit real credentials in
configs/config.yaml.