6.1 KiB
6.1 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 and storage clients.
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.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.