Repositories: ports & adapters

Chitragupt does more than manage the schema — it generates the persistence code your application runs on, and it generates it in a ports-and-adapters (hexagonal) shape. The result: your domain and application layers never touch SQL or even know Postgres exists. They depend on an interface; Chitragupt provides the implementation.

The port: a repository interface

For every aggregate, the generated domain layer declares a repository port — an interface describing how that aggregate is persisted, with no hint of how it's stored:

gen/backend/domain/ordering/v1/ports.go
// OrderRepository — a customer's order, the root of the checkout lifecycle.
type OrderRepository interface {
	fw.Repository[Order, uuid.UUID]
	ListsByCustomer(ctx context.Context, customerID uuid.UUID, opts ...fw.ListOption) (*fw.ListResult[Order], error)
	MarkOrderPaid(ctx context.Context, entity *Order) error
}

Two things are stacked here:

  • The generic contractfw.Repository[Order, uuid.UUID] — the standard CRUD every aggregate gets: Get, List, Create, Update, Delete, plus the filter aggregates (ExistsByFilter, CountByFilter, SumByFilter) that the framework uses to evaluate cross-entity predicates in SQL rather than in Go.
  • The model-specific methodsListsByCustomer (from the ListOrdersByCustomer query) and MarkOrderPaid (from the command). The operations you authored in the application layer become methods on the port.

This interface is the port: the boundary the application core depends on. A command handler that pays an order calls repo.MarkOrderPaid(...) — against the interface, never against a database.

The adapters: Chitragupt (Postgres) and in-memory

An interface needs an implementation — an adapter. Chitragupt generates two, side by side, for every aggregate:

gen/backend/infrastructure/
├── chitragupt/ordering/v1/order_repository.go   ← the Postgres adapter
└── memory/ordering/v1/order_repository.go       ← the in-memory adapter
  • The chitragupt adapter is the real one: it implements OrderRepository against a Postgres connection pool, issuing the SQL, applying row-level security, mapping rows to typed structs.
  • The memory adapter implements the same interface against an in-memory map. It exists so the application core can be exercised in tests with no database at all — same commands, same queries, no Postgres.

Both satisfy OrderRepository exactly, because they are generated from the same port. That is the whole point of the pattern: the implementation is swappable.

Wiring: dependency inversion

The application core is handed a repository; it never constructs one. A generated wiring layer decides which adapter to inject:

gen/backend/wiring/ordering_v1.go
// production — the Chitragupt (Postgres) adapter:
OrderRepo: chitraguptordering.NewOrderRepository(pool),

// tests — the in-memory adapter, same port:
OrderRepo: memory.NewOrderRepository(),

This is dependency inversion: the direction of the dependency points inward, toward the domain. The domain defines the port; infrastructure adapters depend on the domain to implement it; the application core depends on the port, not the adapter. Postgres is a detail plugged in at the edge — swap the adapter and nothing in the domain or application layer changes.

flowchart LR
  subgraph core["application core (no SQL)"]
    CMD[command / query handler] -->|calls| PORT[[OrderRepository<br/>·port·]]
  end
  PORT -.implemented by.-> CH[chitragupt adapter<br/>·Postgres·]
  PORT -.implemented by.-> MEM[memory adapter<br/>·tests·]
  CH -->|SQL| DB[(Postgres)]
  W[wiring] -->|injects one| PORT

Why this matters

You get the benefits of the repository pattern and hexagonal architecture — a testable core, a persistence layer you can reason about, a clean seam between business logic and storage — without writing any of it. No repository interfaces to hand-maintain, no mocks to keep in sync, no SQL scattered through handlers. Chitragupt derives the port from your model, generates the Postgres adapter that backs it and the migrations that create its tables, and generates an in-memory twin so your tests never need a database. The persistence layer is a product of the model, the same way the schema is.