Projektstruktúra
Így néz ki a shop mintaalkalmazás a teljes kiépítés után: két surface-szel, workerrel, eventekkel és migrációkkal:
shop/
├── gpsystem.yaml # generátor-manifest: projektnév, module path, kit verzió,
│ # db (pgx|bun), template checksum, modulok+surface-ök
├── go.mod # require gpsystem + tool directive-ek (gpsystem, oapi-codegen)
├── mise.toml # taskok: setup / spec / generate / dev / restart / build / test
├── Dockerfile # 4 stage: base / development (bind-mount, go run) / build / production
├── .dockerignore
├── compose.yml # teljes dev-stack: postgres, valkey, mailpit, rustfs, migrate,
│ # worker, modul-service-ek, nulla publikált port, Traefik/proxynet
├── .env.example
├── spec/typespec/ # ★ az API kontraktus (source of truth)
│ ├── main.tsp # minden modult importál (anchorral kezelve)
│ ├── lib/ # @permission / @policy dekorátorok
│ ├── shared/errors.tsp # ProblemDetail + FieldError (a httperr tükre)
│ ├── shared/paginator.tsp
│ └── modules/<modul>/ # modulonként: <surface>.tsp + models/
├── api/
│ ├── openapi/ # emittált OpenAPI 3.0 (commitolva, review-olható diffekért)
│ └── oapi-codegen/
│ ├── templates/ # a projekt codegen template-jei (upstream chi-server
│ │ # + a kit minimális strict-felülírásai)
│ └── <modul>-<surface>.yaml
├── cmd/
│ ├── <modul>/main.go # modulonként egy HTTP belépési pont (~20 sor)
│ ├── worker/main.go # háttérfeldolgozó: asynq szerver + outbox relay + scheduler
│ └── migrate/main.go # migrációfuttató
├── internal/
│ ├── platform/config/config.go # összerakott env konfig (Server + DB + Worker + Outbox [+ JWT])
│ └── modules/<modul>/
│ ├── register.go # Dependencies struct + surface-enként egy Register<Surface>(router, deps)
│ ├── core/ # a modul KÖZÖS üzleti logikája: entitások/nézetek, szabályok,
│ │ # sentinel-hibák (pl. ErrNotFound); minden surface ezt használja
│ ├── repository/ # a modul EGYETLEN perzisztencia-rétege (sor-structok +
│ │ # interfész + implementáció); nevesített kapcsolatnál repository/<kapcsolat>/
│ ├── events/ # a modul eventjei (struct + EventName)
│ ├── listeners/ # a modul worker-listenerei
│ │ ├── register.go # events.Listen(...) regisztrációk a workerhez
│ │ └── <név>.go # listener-törzsek
│ ├── jobs/ # a modul worker-jobjai
│ │ ├── register.go # sched.Job(...) regisztrációk a workerhez
│ │ └── <név>.go # job-törzsek
│ ├── mail/ # a modul mail-notifierjei (payload + sablonok)
│ │ └── <név>.go # notifier-törzs + templates/<név>.{html,txt}.tmpl
│ ├── notifications/ # a modul többcsatornás notificationjei (payload + Via)
│ │ └── <név>.go # notification-törzs + templates/<név>.{html,txt}.tmpl
│ └── surfaces/
│ └── <surface>/ # bármely név (api | web | admin | ...); az "api" a modul gyökerére mountol
│ ├── http/handler.go # a generált szigorú interfészt implementálja
│ ├── http/mapper/ # DTO ↔ service típus mappelés (üresen tartva)
│ ├── http/gen/ # oapi-codegen kimenet, soha ne szerkeszd
│ ├── policy/ # policy registry (@permission/@policy érvényesítéshez)
│ └── service/ # vékony, célközönség-specifikus varrat: a core-ba delegál
└── migrations/ # időbélyeg-prefixes SQL migrációk (20200101000000_init.sql, 20200101000100_outbox.sql, ...)
A worker-réteg (cmd/worker, listeners/, jobs/, events/) az új projektekben alapból ott van; ha egy projekt new project --no-worker-rel készült, az add worker adja hozzá.
Mi élhet egy modulon belül
Egy modul mappájában négyféle, egymástól jól elkülönítendő dolog lehet:
| Fajta | Mappák | Mi ez |
|---|---|---|
| Surface-ök | surfaces/api/, surfaces/admin/, ... | Párhuzamos, célközönségenkénti HTTP felületkötegek: http + service + policy |
| Közös mag | core/, repository/ | Amit minden surface megoszt: az üzleti szabályok és entitások (core/), plusz a modul egyetlen perzisztencia-rétege (repository/) |
| Trigger-egységek | jobs/, listeners/ | Alternatív belépési pontok: idő vagy esemény hívja, HTTP helyett; a cmd/worker-be vannak bekötve |
| Kontraktok és tartalom | events/, mail/, notifications/ | Amit a modul termel: event-payloadok, üzenettartalom és sablonok |
api, egy admin, egy web, mind ugyanannak a modulnak egy-egy párhuzamos HTTP-felülete. A core a modul közös üzleti logikája (sima structok metódusokkal, egyszer leírt szabályok), nem DDD-féle "domain layer". A surface-enkénti service a célközönség-specifikus varrat: DDD-fogalmakkal az application service szerepét tölti be, és a közös szabályokhoz a core-ba delegál. Lásd a design pattern-eket az indoklásért.Az ökölszabály: a csatorna/driver-kód a kitben vagy az internal/platform-ban él, a tartalom a modulban, a trigger a jobs//listeners/-ben. A mail a legtisztább példa: az SMTP-transzport (gpsystem/mail, gpsystem/mail/smtp) infrastruktúra, pont úgy mint az adatbázis-driver, a kitben él. Hogy milyen e-mailt küldünk, milyen subjecttel és sablonnal, az a contact modul üzleti tartalma, az internal/modules/contact/mail/-ban él. Lásd a mail oldal kidolgozott példáját.
Ugyanez a szétválasztás egy szinttel feljebb is érvényes a notificationökre: egy notification csatornáit (mail, database, broadcast) a kit adja (gpsystem/notify, plusz a driver-specifikus gpsystem/notify/database és gpsystem/notify/broadcast), míg hogy egy notification mit mond (payload, subject, sablonok), az modultartalom az internal/modules/<modul>/notifications/-ban.
Rétegszabályok
A függőségi irány egyirányú:
http (handler) → service → core → repository
- A handler soha nem nyúl adatbázishoz; inputot köt/validál, a saját surface-e service-ét hívja és eredményt mappel. A handler soha nem importálja a repository csomagot: a sentinel-hibákat (pl. "nincs ilyen rekord") a repository vagy a core definiálja, és a core/service exportálja tovább, így a handler mindig csak a saját surface-e service csomagját ellenőrzi (
errors.Is(err, service.ErrNotFound)). - A surface-enkénti
servicevékony, célközönség-specifikus varrat: a közös szabályokhoz acore-ba delegál, és csak a saját felületének folyamatait tartja. Repository-interfészektől függ, így sima fake-ekkel tesztelhető. - A
corea modul közös üzleti logikája: az entitások/nézetek, az egyszer leírt szabályok és a sentinel-hibák. Eseményt a szabály helyén dobunk, a tranzakción belül, aDependencies-ben kapott dispatcheren át. A core soha nem importál surface-csomagot. - A modulszintű
repository/tartalmazza az SQL-t, a sor-structokat és az interfészt+implementációt, modulonként egyszer, minden surface számára közösen.pg.DBTX-et kap, így ugyanaz a kód fut poolon vagy tranzakcióban, anélkül hogy tudna róla, vagy--db bunprojektben a bun megfelelője. Egy modul repositoryja egy DB engine-t használ; nevesített kapcsolatnálrepository/<kapcsolat>/almappára bomlik (add db). - A
policy/a surface kérés-szintű jogosultsági szabályait tartja: a policy réteg a generált enforcer middleware-en át hívja.
Két bináris, egy életciklus
A cmd/<modul> a HTTP-oldalt futtatja, a cmd/worker a háttérfeldolgozást, mindkettő ugyanarra az app életciklusra épül: azonos graceful shutdown, azonos telemetria-bekötés. Külön skálázod őket: több HTTP-replika, több worker-replika. Az outbox és a scheduler replikabiztos.
Generált vs. saját
Csak a http/gen/ újragenerált kimenet (és jelölve is van). Minden más (handlerek, service-ek, repositoryk, a .tsp fájlok, a listener/job törzsek) normál kód, amit szerkesztesz. A generátorok kizárólag // gpsystem:* anchor-kommenteknél bővítenek meglévő fájlt; lásd a CLI áttekintést.
api/openapi/*.yaml szándékosan commitolva van: az API-változások review-olható diffként jelennek meg a pull requestekben, és a go build soha nem függ a Node toolchaintől.Hova kerüljön a közös kód?
Egy generált projektben nincs pkg/ és nincs internal/pkg/: az internal/ már önmagában privátat jelent, egy második réteg alatta csak egy szemetesfiókot hívna életre. Az internal/platform/config/ az egyetlen app-szintű közös mappa, amivel egy friss projekt indul. Modulon belül a több surface által megosztott szabályok helye a generált core/; lásd a Közös kód: egy döntési létra szekciót arról, hova kerüljön a több surface vagy modul által használt kód.