This page covers conventions for contributing to the Go backend. Read Architecture first if you haven’t already.

Adding a New Domain Entity

Follow this sequence when adding a new entity (e.g. document):

  1. Domaininternal/domain/<name>/entity.go: entity struct, enums, errors
  2. Repository interface — add to internal/repository/interfaces.go
  3. Repository implementationinternal/repository/<group>/<name>.go
  4. Use caseinternal/usecase/<group>/<name>_usecase.go + types file
  5. Handlerinternal/handler/<group>/<name>.go
  6. Routes — wire into internal/server/routes.go
  7. DI — wire repository + use case in internal/app/container.go
  8. Migrationmigrations/<seq>_create_<name>s_table.up.sql
  9. Mocks — add interface to go:generate in internal/repository/interfaces.go, run just generate-mocks
  10. Tests — unit test the use case with mocks; integration test the repository with testcontainers

Naming Conventions

  • Package names: short, lowercase, singular — user, project, support
  • Repository files: internal/repository/<group>/<entity>.go (e.g. repository/person/person.go)
  • Handler files: internal/handler/<group>/<entity>.go (e.g. handler/project/person.go)
  • Use case files: internal/usecase/<group>/<entity>_usecase.go
  • Entity IDs: ulid.ULID in structs, string in DTOs (via .String())
  • Unexported repo structs: type userRepository struct { db *sqlx.DB }
  • Constructor: func New(db *sqlx.DB) repository.UserRepository

Handler Pattern

Bind, call, respond.

func (h *PersonHandler) Create(c *gin.Context) {
    req, ok := handler.BindJSON[CreatePersonRequest](c)
    if !ok {
        return
    }

    projectID := c.Param("project_id")
    userID, _ := middleware.UserIDFrom(c)

    out, err := h.personUC.Create(c.Request.Context(), usecase.CreatePersonInput{
        ProjectID: projectID,
        CreatedBy: userID.String(),
        // ... fields from req
    })
    if err != nil {
        handler.HandleError(c, err)
        return
    }

    c.JSON(http.StatusCreated, out)
}

handler.BindJSON[T] and handler.HandleError are defined in internal/handler/errors.go and imported by all handler subdirectory packages. BindJSON decodes the request body and writes a 400 response on failure, returning false so the handler can return immediately.

Use Case Pattern

type CreatePersonInput struct {
    ProjectID string
    FirstName string
    LastName  string
    // ...
}

type CreatePersonOutput struct {
    PersonID string `json:"person_id"`
}

func (uc *PersonUseCase) Create(ctx context.Context, in CreatePersonInput) (*CreatePersonOutput, error) {
    person := &domain.Person{
        ID:        ulid.New(),
        ProjectID: in.ProjectID,
        FirstName: in.FirstName,
        // ...
        CreatedAt: time.Now().UTC(),
        UpdatedAt: time.Now().UTC(),
    }

    if err := uc.repo.Create(ctx, person); err != nil {
        return nil, err
    }

    return &CreatePersonOutput{PersonID: person.ID.String()}, nil
}

Error Handling

Domain errors are defined in internal/domain/<name>/errors.go:

var (
    ErrPersonNotFound = errors.New("person not found")
    ErrPersonExists   = errors.New("person already exists")
)

The handler maps domain errors to HTTP status codes in internal/handler/errors.go. Add new errors there when needed. Do not return raw database errors from use cases.

Migrations

Forward-only only — no .down.sql files. Filename pattern:

<seq>_<description>.up.sql

Where <seq> is a zero-padded 6-digit number. Create with:

observer migrate create <description>
# or
just migrate-create <description>

Dependency Injection

All wiring happens in internal/app/container.go. The pattern:

// 1. Create repo (each domain group has its own package)
personRepo := repoperson.New(sqlxDB)

// 2. Create use case
personUC := ucproject.NewPersonUseCase(personRepo, ...)

// 3. Store in container
c.PersonUC = personUC

Then in internal/server/routes.go, inject into the handler:

personHandler := projecthandler.NewPersonHandler(container.PersonUC, ...)

Code Style

  • No decorative comment separators (//-----, //=====)
  • Docstrings only on exported symbols
  • Complex logic: prefer a Mermaid diagram in a module README over inline comments
  • gofmt formatting enforced — run just fmt before committing
  • Linting: just lint (golangci-lint)