| name | feature-generator |
| description | Generate full-stack features. Backend = hand-written bounded-context aggregates (DDD); frontend = FSD slices with HydrationBoundary. Use when creating new features, adding CRUD operations, or scaffolding new pages. |
| allowed-tools | Read, Edit, Write, Bash, Glob, Grep |
Feature Generator
Create complete full-stack features. Backend follows bounded contexts (DDD-strategic) with Clean-Architecture layers (DDD-tactical) inside each context. Frontend follows Feature-Sliced Design.
No code generator is used. The backend used to be scaffolded with Goca; it was removed in refactor/backend-clean-architecture because its flat-layer output is structurally incompatible with this repo's bounded-context layout. The five backend files per aggregate are short — copy the existing internal/stats/ context as the canonical template and modify.
Backend (Go) — bounded-context layout
Each aggregate lives inside one bounded context (e.g. stats/, auth/, notifications/, exports/, or a brand-new one). Five files plus a wiring step:
- Domain —
backend/internal/<ctx>/domain/<aggregate>.go
- Pure types only. No
gorm.io/gorm import. No I/O.
- Embed
shared.AggregateBase if it raises events.
- Define value objects with constructor invariants (
func NewMoney(...) (Money, error)).
- Define domain events implementing
EventName() string.
- Application port + use case —
backend/internal/<ctx>/application/
ports.go declares interfaces (Repository, JobEnqueuer, ...).
<aggregate>_usecases.go holds use-case structs with Execute(ctx, ...).
- Pull events with
agg.PullEvents() before repo.Save(...).
- Persistence —
backend/internal/<ctx>/infrastructure/persistence/
gorm_models.go (unexported GORM-tagged twin) + <aggregate>_mapper.go + <aggregate>_repo.go + registry.go exposing Entities() []any.
- Assert the port:
var _ <ctx>app.Repository = (*Repository)(nil).
Save must mutate only DB-owned fields back into *agg, never replace it whole (would wipe pending events).
- HTTP adapter —
backend/internal/<ctx>/interfaces/http/handler.go
- Depends only on this context's
application/ package. Never imports gorm or another bounded context.
- Swagger annotations on every endpoint.
- Wire in composition root —
backend/internal/composition/composition.go
- Build repo → use cases → handler. Register routes. Append
<ctx>persist.Entities() to runAutoMigrations.
- If cross-context data is needed, add an Anti-Corruption Layer adapter right here (mirror
statsToExportsReader / authToNotificationsDirectory).
Frontend (React)
- Server Component for initial loading (no flicker)
- Client Component for interactivity
- Generated API hooks via Orval
- UI components with shadcn/ui
Quick Start: Copy the Stats Context
ls backend/internal/stats/
mkdir -p backend/internal/products/{domain,application,infrastructure/persistence,interfaces/http}
cd .. && just api
just dev-backend
Entity Registry (AutoMigrate)
There is no central registry. Each bounded context owns its own Entities() function under internal/<ctx>/infrastructure/persistence/registry.go. The composition root aggregates them:
func Entities() []any {
return []any{&gormProduct{}}
}
entities = append(entities, productspersist.Entities()...)
Server-Side Data Loading Pattern (HydrationBoundary)
Step 1: Server Component with prefetchQuery
import { dehydrate, HydrationBoundary } from "@tanstack/react-query"
import { cookies } from "next/headers"
import { redirect } from "next/navigation"
import { getGetProductsQueryKey, getProducts } from "@shared/api/endpoints/products/products"
import { getQueryClient } from "@shared/lib/query-client"
import { getSession } from "@shared/lib/auth-server"
import { ProductList } from "./product-list"
export default async function ProductsPage() {
const session = await getSession()
if (!session) redirect("/login")
const cookieStore = await cookies()
const cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ")
const queryClient = getQueryClient()
await queryClient.prefetchQuery({
queryKey: getGetProductsQueryKey(),
queryFn: () => getProducts({ headers: { Cookie: cookieHeader }, cache: "no-store" }),
})
return (
<HydrationBoundary state={dehydrate(queryClient)}>
<div className="container py-8">
<h1 className="text-2xl font-bold mb-4">Products</h1>
<ProductList />
</div>
</HydrationBoundary>
)
}
Step 2: Client Component (no initialData needed)
"use client"
import { useGetProducts } from "@shared/api/endpoints/products/products"
import { useSSE } from "@features/stats"
export function ProductList() {
useSSE()
const { data: productsResponse } = useGetProducts()
const products = productsResponse?.status === 200 ? productsResponse.data : null
return (
<div className="grid gap-4">
{products?.map((product) => (
<div key={product.id}>{product.name} - {product.price}€</div>
))}
</div>
)
}
Backend Handler with Swagger
func (h *Handler) GetProducts(w http.ResponseWriter, r *http.Request) {
}
After Backend Changes
just api
Conventions
File Naming
- Go:
snake_case.go
- React Pages:
page.tsx in route folder
- Client Components:
kebab-case.tsx
Route Protection
- Public:
frontend/src/app/
- Protected:
frontend/src/app/(protected)/
- Auth:
frontend/src/app/(auth)/
No Skeleton/Flicker
- Server Component loads data before rendering
- Client Component reads from the hydrated cache (no
initialData prop needed)
- React Query takes over for updates
- SSE for real-time sync