Navigating the generated application

After crank init, you have a fully wired Go backend service.
This page explains every layer โ€” where things live, why they are there, how they connect, and how to extend them.

Go version Echo v4 DDD CQRS License



The generated project follows a Domain-Driven Design layered architecture.
Every layer depends only on the layer below it โ€” never upward.

flowchart TB subgraph Presentation["๐ŸŒ Presentation"] direction TB H["Echo Handlers<br/><code>internal/adapters/http/web/</code>"] end subgraph Application["โš™๏ธ Application"] direction TB C["Command & Query Handlers<br/><code>internal/application/</code>"] end subgraph Domain["๐Ÿง  Domain"] direction TB A["Aggregates & Value Objects<br/><code>internal/domain/</code>"] E["Domain Events<br/><code>internal/domain/shared/</code>"] P["Repository Interfaces<br/><code>internal/domain/</code>"] end subgraph Infrastructure["๐Ÿ”Œ Infrastructure"] direction TB PER["Persistence Adapters<br/><code>internal/adapters/persistence/</code>"] EB["Event Bus<br/><code>internal/adapters/eventbus/</code>"] UOW["Unit of Work<br/><code>internal/adapters/uow/</code>"] end subgraph Cross["๐Ÿ”ง Cross-Cutting"] direction TB CFG["Configuration<br/><code>internal/config/</code>"] VAL["Validation<br/><code>internal/validator/</code>"] LOG["Logging<br/><code>pkg/logging/</code>"] end H --> C C --> A C --> E C --> P C --> UOW P --> PER UOW --> PER UOW --> EB A --> E
Principle How itโ€™s enforced
๐Ÿงน Domain is pure Go Zero framework imports in internal/domain/ โ€” no HTTP, no database drivers, no serialization tags
โฌ‡๏ธ Dependencies point inward Adapters implement interfaces defined by the domain; the domain never imports adapters
๐ŸŽฏ Application coordinates Handlers load aggregates, call domain methods, and route persistence + events through ports
๐Ÿ”Œ Composition root cmd/server/main.go is the only place concrete types meet interfaces
HTTP Handlers  โ†’  Application Handlers  โ†’  Domain (Aggregates + Ports)  โ†  Infrastructure Adapters
     โ”‚                    โ”‚                                                    โ”‚
     โ””โ”€โ”€โ”€ DTOs live โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                                    โ”‚
          here                                                                 โ”‚
                                                                               โ”‚
          Infrastructure adapters (Persistence, EventBus, etc.) implement      โ”‚
          interfaces defined in the Domain โ€” the domain never                  โ”‚
          imports infrastructure.                                              โ”‚

myapp/
โ”œโ”€โ”€ ๐Ÿ“‚ cmd/server/                  โ†  Entry point + composition root
โ”œโ”€โ”€ ๐Ÿ“‚ configs/                     โ†  Config defaults (committed)
โ”œโ”€โ”€ ๐Ÿ“‚ internal/
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ adapters/                โ†  Infrastructure implementations
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ eventbus/            โ†  In-process event bus
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ http/web/            โ†  Echo handlers + middleware
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ persistence/         โ†  memory/, bun/, gorm/ repositories
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ cache/               โ†  Redis client (redis feature)
โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ uow/                 โ†  Unit of Work implementations
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ application/             โ†  CQRS use cases
โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ user/
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ config/                  โ†  Viper + env config loading
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ domain/                  โ†  Pure domain model
โ”‚   โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ shared/              โ†  DomainEvent interface + codecs
โ”‚   โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ user/                โ†  Aggregate, ID, events, errors, port
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ model/                   โ†  Shared DTOs (APIError)
โ”‚   โ”œโ”€โ”€ ๐Ÿ“‚ ports/                   โ†  Cross-cutting interfaces
โ”‚   โ””โ”€โ”€ ๐Ÿ“‚ validator/               โ†  Request validation
โ”œโ”€โ”€ ๐Ÿ“‚ pkg/logging/                 โ†  slog helpers + redaction
โ”œโ”€โ”€ ๐Ÿ“‚ docs/                        โ†  Generated Swagger spec
โ”œโ”€โ”€ ๐Ÿ“‚ migrations/                  โ†  SQL migrations (with ORM)
โ”œโ”€โ”€ ๐Ÿ“„ .crank.yaml                  โ†  Project manifest (do not delete)
โ”œโ”€โ”€ ๐Ÿ“„ .env.example                 โ†  Local env template
โ”œโ”€โ”€ ๐Ÿ“„ .air.toml                    โ†  Live-reload config
โ”œโ”€โ”€ ๐Ÿ“„ Dockerfile
โ””โ”€โ”€ ๐Ÿ“„ Makefile                     โ†  Project-specific targets

๐Ÿ’ก Tip: The internal/ directory enforces Goโ€™s visibility rules โ€”
nothing outside the module can import packages under internal/.


This is the single entry point (cmd/server/main.go) and the only place where concrete types
are wired together. It performs these steps in order:

flowchart LR A["1. Load Config<br/><code>config.Load()</code>"] --> B["2. Init Logger<br/><code>slog</code>"] B --> C["3. Connect Infra<br/><code>DB / Redis / etc.</code>"] C --> D["4. Wire Domain<br/><code>Bus / Repo / UoW</code>"] D --> E["5. Create App Services<br/><code>Cmd + Qry Handlers</code>"] E --> F["6. Create HTTP Handlers<br/><code>Echo handlers</code>"] F --> G["7. Mount Routes<br/><code>web.Mount()</code>"] G --> H["8. Start Server<br/><code>e.Start(addr)</code>"] H --> I["SIGINT/SIGTERM<br/><code>Graceful shutdown</code>"]

This is also where feature clients (Redis, Temporal, MongoDB, etc.) are wired.
The generated code uses marker comments (// crank:config-*, // crank:http-*)
so that crank add and crank make handler can inject new wiring without
breaking existing code.

Client Initialized when
eventbus.NewInMemory() Always
memory.NewUserRepository() No ORM feature
bun.NewUserRepository(db) bun feature
gorm.NewUserRepository(gormDB) gorm feature
redisclient.NewClient(cfg.Redis) redis feature
mongodb.NewClient(ctx, cfg.MongoDB) mongodb feature
qdrantclient.NewClient(ctx, cfg.Qdrant) qdrant feature
temporal.NewClient(cfg.Temporal, logger) temporal feature
telemetry.NewProvider(ctx, cfg.Telemetry) otel feature
Outbox UoW + Worker outbox feature

The domain layer (internal/domain/) is the heart of the application โ€” pure Go with zero
external dependencies. Each business concept lives in its own package.

Taking the generated internal/domain/user/ package:

File ๐ŸŽฏ Purpose
user.go Aggregate root struct + constructor + behavior methods + event recording
user_id.go Typed value object โ€” prevents passing the wrong ID type at compile time
events.go Domain events: UserCreated, UserUpdated, UserDeleted
errors.go Sentinel errors: ErrUserNotFound, ErrInvalidUser
repository.go Port (interface) โ€” implementations live in adapters
type User struct {
    id        UserID
    name      string
    email     string
    password  string       // bcrypt hash (only with auth feature)
    createdAt time.Time
    updatedAt time.Time
    events    []shared.DomainEvent
}
Decision Rationale
๐Ÿ”’ Unexported fields The aggregate controls its own state through methods. No one can set user.name directly.
๐Ÿท๏ธ No struct tags No json, db, validate, gorm, or bun tags. Each layer defines its own DTOs.
โŒ Methods return errors Constructors and mutators validate invariants (empty name โ†’ ErrInvalidUser).
๐Ÿ“ก Event sourcing ready Behavior methods record events via recordEvent. Call PullEvents() to collect and dispatch.
id, _ := user.NewUserID("usr_123")
u, err := user.NewUser(id, "Alice", "alice@example.com")
// u.PullEvents() โ†’ [UserCreated{...}]

The internal/domain/shared/ package provides:

Component Purpose
DomainEvent interface EventName() string, OccurredAt() time.Time
EncodeEvent() / DecodeEvent() JSON serialize/deserialize for transport
EventEnvelope Wire format: name + timestamp + JSON body
Event factory registry Maps event names to constructors for reliable deserialization

The application layer (internal/application/) implements use cases using the CQRS pattern โ€”
Commands mutate state, Queries read state. They never mix.

flowchart LR subgraph Commands["๐Ÿ“ Commands"] CC["CreateUserCommand"] UC["UpdateUserCommand"] DC["DeleteUserCommand"] end subgraph Queries["๐Ÿ” Queries"] GQ["GetUserQuery"] LQ["ListUsersQuery"] end subgraph Handlers["๐ŸŽฎ Handlers"] CH["CommandHandler<br/><code>HandleCreate</code><br/><code>HandleUpdate</code><br/><code>HandleDelete</code>"] QH["QueryHandler<br/><code>HandleGet</code><br/><code>HandleList</code>"] end subgraph Infra["๐Ÿ”Œ Infrastructure"] UOW["Unit of Work"] BUS["Event Bus"] REPO["Repository"] end Commands --> CH Queries --> QH CH --> UOW CH --> REPO UOW --> BUS QH --> REPO
func (h *CommandHandler) HandleCreate(ctx context.Context, cmd CreateUserCommand) (*user.User, error) {
    id, err := user.NewUserID(cmd.ID)
    u, err := user.NewUser(id, cmd.Name, cmd.Email)
    err := h.uow.SaveAndPublish(ctx,
        func(ctx context.Context) error { return h.repo.Save(ctx, u) },
        u.PullEvents(),
    )
    return u, nil
}

โšก The command handler never calls repo.Save directly โ€” it goes through
the Unit of Work to ensure the aggregate save and event publication
are handled atomically.

Benefit Explanation
๐Ÿš€ Optimize reads independently Swap in a read-optimized store (cached view, materialized table) without touching command logic
๐ŸŽฏ Handlers stay focused Command handlers handle mutations + events; query handlers do pure reads
๐Ÿงฉ Clear intent A POST handler calls cmd.HandleCreate, a GET handler calls qry.HandleGet

The presentation layer (internal/adapters/http/web/) โ€” translates HTTP requests to application commands/queries
and results back to JSON.

File ๐ŸŽฏ Role
server.go Creates Echo instance, wires the smart binder, exposes GET /health
routes.go Central route aggregator โ€” generated handlers splice in via marker comments
user_handler.go CRUD handler โ€” DTOs, Swagger annotations, error mapping
middleware/logging.go Request-scoped logger injection
middleware/auth.go JWT middleware (with auth feature)

The generated server replaces Echoโ€™s default binder with a custom echoBinder
that automatically validates after every c.Bind():

type echoBinder struct {
    defaultBinder echo.Binder
    logger        *slog.Logger
}

func (b *echoBinder) Bind(i any, c echo.Context) error {
    if err := b.defaultBinder.Bind(i, c); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }
    if err := validator.Struct(i); err != nil {
        return err  // automatically formatted as ValidationError
    }
    return nil
}

This means handlers never call the validator explicitly:

func (h *UserHandler) Create(c echo.Context) error {
    var in userDTO
    if err := c.Bind(&in); err != nil {  // Bind + validate in one call
        return err
    }
    // โœ… `in` is guaranteed valid past this point
    out, err := h.cmd.HandleCreate(c.Request().Context(), in.toCreateCommand())
    // ...
}

HTTP handlers define their own DTOs with JSON and validation tags โ€”
they are not the domain aggregate:

type userDTO struct {
    ID    string `json:"id"    validate:"required"`
    Name  string `json:"name"  validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
}

func (d userDTO) toCreateCommand() appuser.CreateUserCommand {
    return appuser.CreateUserCommand{ID: d.ID, Name: d.Name, Email: d.Email}
}

routes.go uses marker comments to make code generation idempotent:

type MountConfig struct {
    UserHandler *UserHandler
    // crank:http-fields โ† new handlers splice fields here
}

func Mount(e *echo.Echo, cfg MountConfig) {
    g := e.Group("/users")
    cfg.UserHandler.Register(g)
    // crank:http-register โ† new handlers splice registrations here
}

The composition root installs a custom HTTPErrorHandler that:

  1. ๐ŸŸก ValidationError โ†’ 422 with field-level error details
  2. ๐Ÿ”ด echo.HTTPError โ†’ Appropriate HTTP status code
  3. โšซ Unhandled errors โ†’ 500 Internal Server Error

Handlers map domain errors to HTTP status codes:

if errors.Is(err, user.ErrUserNotFound) {
    return c.JSON(http.StatusNotFound, model.APIError{Error: "user not found"})
}

These (internal/adapters/persistence/) implement the domainโ€™s Repository interface. The composition root
decides which one to wire in โ€” no application code changes.

Adapter Location ๐ŸŽฏ Use Case
๐Ÿง  In-memory persistence/memory/ Unit tests, local dev without database
๐Ÿ˜ GORM persistence/gorm/ Production PostgreSQL (with gorm feature)
๐Ÿ˜ Bun persistence/bun/ Production PostgreSQL (with bun feature)

The in-memory repository is a thread-safe sync.RWMutex-guarded map โ€”
perfect for fast tests:

type UserRepository struct {
    mu    sync.RWMutex
    byID  map[string]*user.User
    byEml map[string]*user.User
}

To switch from in-memory to PostgreSQL, change only the composition root:

// In-memory (default)
userRepo := memory.NewUserRepository()

// Bun
userRepo := bun.NewUserRepository(db)

// GORM
userRepo := gorm.NewUserRepository(gormDB)

No application or HTTP code changes โ€” they depend only on the domainโ€™s Repository interface.


These two abstractions ensure atomicity between persisting an aggregate
and publishing its domain events.

flowchart LR subgraph Handler["๐ŸŽฎ Application Handler"] HC["HandleCreate"] end subgraph UOW["๐Ÿ“ฆ Unit of Work"] S["1. Save Aggregate"] P["2. Publish Events"] end subgraph Infra["๐Ÿ”Œ Infrastructure"] R["Repository"] B["Event Bus"] end HC --> UOW S --> R P --> B

The default implementation runs save then publish sequentially:

Step Action On failure
1 repo.Save(ctx, user) โŒ Short-circuit โ€” events not published
2 bus.Publish(ctx, events) โš ๏ธ Logged โ€” save stands

Both operations share a database transaction:

  1. Aggregate saved to its table
  2. Domain events written to outbox_events table in same transaction
  3. Background worker polls outbox table and relays events to the bus
  4. Events deleted after successful publication (at-least-once delivery)

This eliminates the window between โ€œsave succeededโ€ and โ€œpublish failedโ€
that the in-memory UoW has.

The in-memory bus is a simple pub/sub:

bus := eventbus.NewInMemory()
bus.Subscribe(func(ctx context.Context, ev shared.DomainEvent) error {
    slog.Default().Info("event received", "event", ev.EventName())
    return nil
})

Subscribers are called synchronously in registration order. A failing
subscriber does not block others โ€” the error is logged and the next one runs.


Priority:  env vars  >  .env file  >  configs/config.yaml
File Committed? Use
configs/config.yaml โœ… Yes Non-secret defaults (host, port, log level)
.env.example โœ… Yes Template for developers
.env โŒ No Local secrets, gitignored

Secret fields use env:"SECRET_NAME" tags and are loaded from .env
or environment variables. Feature configs are injected via marker comments.

Preconfigured go-playground/validator singleton:

  • Uses JSON tag names in error messages
  • Automatically called by the smart Echo binder
  • Extensible โ€” add custom validators in init():
validate.RegisterValidation("notblank", func(fl validator.FieldLevel) bool {
    return strings.TrimSpace(fl.Field().String()) != ""
})

Shared response types used by every HTTP handler:

type APIError struct {
    Error   string `json:"error"`
    Details any    `json:"details,omitempty"`
}

Helpers built on log/slog:

Function Purpose
logging.New(level, addSource) Creates a configured logger
logging.FromContext(ctx) Retrieves request-scoped logger
logging.Redacted(key, value) Redacts sensitive values (passwords, tokens)

Cross-cutting interfaces that adapters implement:

Interface Purpose Default implementation
EventBus Publish domain events In-memory bus
UnitOfWork Atomic save + publish In-memory UoW
Hasher Hash/compare passwords BCrypt (with auth)
TokenService Issue/refresh/validate JWT JWT service (with auth)
Cache Get/Set/Delete cache entries (with redis)
Cipher Encrypt/Decrypt data AES-256-GCM (with crypto)
TracerProvider Create OTel tracers Stdout exporter (with otel)

                          POST /users
                      {"name":"Alice","email":"a@b.com"}
                              โ”‚
                              โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Echo Router                        โ”‚
โ”‚  Matches POST /users โ†’ UserHandler.Create            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              echoBinder.Bind(&userDTO)                โ”‚
โ”‚                                                       โ”‚
โ”‚   1. DefaultBinder.Bind() โ†’ parse JSON into struct    โ”‚
โ”‚   2. validator.Struct()    โ†’ validate tags            โ”‚
โ”‚                                                       โ”‚
โ”‚   โ”€โ”€ On failure โ”€โ”€โ†’ 422 ValidationError              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚               UserHandler.Create(c)                   โ”‚
โ”‚                                                       โ”‚
โ”‚   userDTO.toCreateCommand() โ†’ CreateUserCommand       โ”‚
โ”‚   cmd.HandleCreate(ctx, cmd)                          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           Application CommandHandler                  โ”‚
โ”‚                                                       โ”‚
โ”‚   1. user.NewUserID(cmd.ID)                           โ”‚
โ”‚   2. user.NewUser(id, name, email)                    โ”‚
โ”‚       โ†’ User aggregate + UserCreated event            โ”‚
โ”‚   3. uow.SaveAndPublish(ctx, saveFn, events)          โ”‚
โ”‚       โ”œโ”€โ”€ repo.Save(ctx, user)                        โ”‚
โ”‚       โ””โ”€โ”€ bus.Publish(ctx, UserCreated{...})          โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                   โ”‚
                                   โ–ผ
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                   Response                            โ”‚
โ”‚                                                       โ”‚
โ”‚   toUserDTO(result) โ†’ 201 Created                     โ”‚
โ”‚   { "id":"...", "name":"Alice", "email":"a@b.com" }  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each optional feature adds files to the project. Here is what every feature
contributes and where to find it.

๐Ÿ” Auth โ€” JWT authentication + bcrypt
File Purpose
internal/ports/hasher.go Password hashing interface
internal/ports/tokenservice.go Token issue/refresh/validation interface
internal/adapters/crypto/bcrypt_hasher.go BCrypt hashing
internal/adapters/crypto/jwt_token_service.go JWT token management
internal/adapters/http/web/auth_handler.go /auth/register, /auth/login, /auth/refresh, /me
internal/adapters/http/web/middleware/auth.go JWTAuth() middleware
internal/domain/user/email.go Email value object
internal/domain/user/password.go Password value object

Endpoints:

Method Path Auth Description
POST /auth/register โŒ Create account + return tokens
POST /auth/login โŒ Authenticate + return tokens
POST /auth/refresh โŒ Exchange refresh token for new pair
GET /me โœ… Bearer Current user ID from JWT

Protecting custom routes:

e.GET("/admin", adminHandler, middleware.JWTAuth(tokens))
๐Ÿ—„๏ธ Bun ORM โ€” PostgreSQL via Bun
File Purpose
internal/adapters/persistence/bun/db.go PostgreSQL connection with pgdriver
internal/adapters/persistence/bun/migrate.go Migration runner
internal/adapters/persistence/bun/user_repository.go Bun-backed user repository
migrations/000001_init.up.sql Initial schema
migrations/000001_init.down.sql Schema rollback

๐Ÿ“– bun.uptrace.dev

๐Ÿ—„๏ธ GORM โ€” PostgreSQL via GORM
File Purpose
internal/adapters/persistence/gorm/db.go PostgreSQL connection
internal/adapters/persistence/gorm/migrate.go Migration runner
internal/adapters/persistence/gorm/user_repository.go GORM-backed user repository
migrations/000001_init.up.sql Initial schema
migrations/000001_init.down.sql Schema rollback

๐Ÿ“– gorm.io/docs

โšก Redis โ€” Caching client
File Purpose
internal/ports/cache.go Cache interface
internal/adapters/cache/redis/client.go redis.Client connection

Wired in the composition root as rdb. ๐Ÿ“– redis.uptrace.dev

๐Ÿƒ MongoDB โ€” Document database client
File Purpose
internal/adapters/persistence/mongodb/client.go mongo.Client connection

Wired as mdb. Access via mdb.Client().Database("name").Collection("coll").
๐Ÿ“– mongodb.com/docs/drivers/go

๐Ÿง  Qdrant โ€” Vector database client
File Purpose
internal/adapters/persistence/qdrant/client.go Qdrant gRPC client

๐Ÿ“– qdrant.tech/documentation

โฐ Temporal โ€” Workflow orchestration
File Purpose
internal/adapters/temporal/client.go Temporal client + slog bridge
internal/adapters/temporal/worker.go Worker with marker-based registration
internal/adapters/temporal/workflow/greeting.go Example workflow
internal/adapters/temporal/activity/greeting.go Example activity
cmd/worker/main.go Standalone worker entry

Generate new workflows:

crank make workflow OrderFulfillment order_id:uuid
crank make activity ChargeCard amount:float --tests

๐Ÿ“– docs.temporal.io/dev-guide/go

๐Ÿ“Š OpenTelemetry โ€” Distributed tracing
File Purpose
internal/ports/tracer.go TracerProvider interface
internal/adapters/telemetry/otel.go OTel SDK setup (stdout exporter)
internal/adapters/http/web/middleware/tracing.go HTTP tracing middleware

Emits spans to stdout by default. Swap to OTLP for production.
๐Ÿ“– opentelemetry.io/docs/languages/go

๐Ÿ”’ Crypto โ€” AES-256-GCM encryption
File Purpose
internal/ports/cipher.go Encrypt/Decrypt interface
internal/adapters/crypto/aesgcm/cipher.go AES-256-GCM implementation

Generate a strong key: openssl rand -base64 32

๐ŸŽจ Views โ€” React SPA
File Purpose
internal/adapters/http/web/views.go SPA serving + Vite proxy
static/embed.go Embedded static assets
src/App.jsx React application
vite.config.js Vite dev server config

Toggle with views.enabled in config. Set views.dev_server for HMR.

๐Ÿ“ค Outbox โ€” Transactional outbox pattern
File Purpose
internal/domain/outbox/event.go Outbox event domain type
internal/domain/outbox/repository.go Outbox repository port
internal/adapters/persistence/bun/outbox_repository.go Bun-backed outbox repository
internal/adapters/persistence/gorm/outbox_repository.go GORM-backed outbox repository
internal/adapters/outbox/bun_uow.go Bun transactional UoW
internal/adapters/outbox/gorm_uow.go GORM transactional UoW
internal/adapters/outbox/worker.go Background poller + publisher
migrations/000002_add_outbox_events.up.sql Outbox table schema

Requires gorm or bun. Replaces in-memory UoW with transaction-backed.


crank make scaffold Product title:string price:float --tests
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                    crank make scaffold                    โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                                           โ”‚
โ”‚  ๐Ÿ“‚ internal/domain/product/                              โ”‚
โ”‚  โ”œโ”€โ”€ product.go              โ† Aggregate root             โ”‚
โ”‚  โ”œโ”€โ”€ product_id.go           โ† Typed ID                   โ”‚
โ”‚  โ”œโ”€โ”€ events.go               โ† Domain events              โ”‚
โ”‚  โ”œโ”€โ”€ errors.go               โ† Sentinel errors            โ”‚
โ”‚  โ””โ”€โ”€ repository.go           โ† Port                       โ”‚
โ”‚                                                           โ”‚
โ”‚  ๐Ÿ“‚ internal/application/product/                         โ”‚
โ”‚  โ”œโ”€โ”€ commands.go             โ† Create/Update/Delete       โ”‚
โ”‚  โ”œโ”€โ”€ command_handler.go      โ† Command execution           โ”‚
โ”‚  โ”œโ”€โ”€ queries.go              โ† Get/List                   โ”‚
โ”‚  โ””โ”€โ”€ query_handler.go        โ† Query execution            โ”‚
โ”‚                                                           โ”‚
โ”‚  ๐Ÿ“‚ internal/adapters/persistence/memory/                 โ”‚
โ”‚  โ””โ”€โ”€ product_repository.go   โ† In-memory implementation   โ”‚
โ”‚                                                           โ”‚
โ”‚  ๐Ÿ“‚ internal/adapters/http/web/                           โ”‚
โ”‚  โ””โ”€โ”€ product_handler.go      โ† Echo handler + DTOs        โ”‚
โ”‚                                                           โ”‚
โ”‚  In routes.go:               โ† Route wiring (idempotent)  โ”‚
โ”‚  In migrations/:             โ† SQL migration (bun only)   โ”‚
โ”‚  _test.go files:             โ† With --tests flag          โ”‚
โ”‚                                                           โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
Kind ๐ŸŽฏ Primary artifact ๐Ÿ”„ Dependencies generated
model Domain aggregate, ID, events, errors, port โ€”
repository Repository implementation Domain model (if missing)
service Commands, queries, handlers Domain + repository (if missing)
handler HTTP handler + route wiring Domain + service + repo (skipped with --only)
scaffold Everything above Everything above
workflow Temporal workflow + registration โ€”
activity Temporal activity + registration โ€”
migration SQL up/down pair โ€”

โš ๏ธ Idempotency: Running crank make scaffold Product twice errors on
the primary artifact but skips existing dependency files. Use --force
to overwrite. Route wiring is idempotent โ€” no duplicate registrations.


The in-memory adapters make it possible to test every layer without a database.

No mocks needed โ€” pure Go types:

func TestNewUser(t *testing.T) {
    id, _ := user.NewUserID("test-1")
    u, err := user.NewUser(id, "Alice", "alice@example.com")
    assert.NoError(t, err)
    assert.Equal(t, "Alice", u.Name())
    events := u.PullEvents()
    assert.Len(t, events, 1)
    assert.Equal(t, "user.created", events[0].EventName())
}

Wire in-memory adapters directly โ€” no database required:

func TestCreateUser_CommandHandler(t *testing.T) {
    repo := memory.NewUserRepository()
    bus := eventbus.NewInMemory()
    uow := uow.NewInMemoryUoW(bus)
    handler := userapp.NewCommandHandler(repo, uow)

    user, err := handler.HandleCreate(context.Background(), user.CreateUserCommand{
        ID: "test-1", Name: "Alice", Email: "alice@example.com",
    })
    assert.NoError(t, err)
    assert.Equal(t, "Alice", user.Name())
}

Use Echoโ€™s httptest helpers:

func TestUserHandler_Create(t *testing.T) {
    // Wire in-memory adapters
    repo := memory.NewUserRepository()
    bus := eventbus.NewInMemory()
    uow := uow.NewInMemoryUoW(bus)
    cmd := userapp.NewCommandHandler(repo, uow)
    qry := userapp.NewQueryHandler(repo)
    handler := web.NewUserHandler(cmd, qry)

    // Echo test
    e := echo.New()
    body := `{"name":"Alice","email":"a@b.com"}`
    req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    err := handler.Create(c)
    assert.NoError(t, err)
    assert.Equal(t, http.StatusCreated, rec.Code)
}
Layer Approach ๐ŸŽฏ What to test
๐Ÿง  Domain Pure Go unit tests Invariants, event recording, error cases
โš™๏ธ Application In-memory repo + bus Command/query logic, event publication
๐ŸŒ HTTP Echo httptest helpers Routing, status codes, validation, response shapes
๐Ÿ”Œ Persistence Real DB or in-memory Save/get/delete round-trips

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              godotenv.Load()                      โ”‚
โ”‚  Reads .env into process environment              โ”‚
โ”‚  (silently skipped if .env doesn't exist)         โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚              viper.ReadInConfig()                 โ”‚
โ”‚  Reads configs/config.yaml                        โ”‚
โ”‚  Sets defaults via setDefaults()                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ”‚
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           env.ParseWithOptions(&cfg)              โ”‚
โ”‚  Overlays env vars on fields tagged env:"..."     โ”‚
โ”‚  (typically secrets only)                        โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                       โ”‚
                       โ–ผ
                  โœ… Final Config
  1. Add the field to Config struct in internal/config/config.go
  2. Add a Viper default in setDefaults()
  3. Add the YAML key to configs/config.yaml
  4. If itโ€™s a secret, add env:"VAR_NAME" tag and document in .env.example

When you run crank add redis, new config fields are injected at marker comments:

type Config struct {
    App AppConfig `mapstructure:"app"`
    // crank:config-fields       โ† RedisConfig inserted here
    Logging LoggingConfig `mapstructure:"logging"`
}

The injection is idempotent โ€” adding the same feature twice is a no-op.



crank logo
crank โ€” Scaffold. Build. Ship.