golang-mvc
Go MVC implementation conventions for feature work and refactoring.
This skill guides code generation and actively enforces layer boundaries —
it modifies and refactors code that violates MVC conventions.
Layer Map
| Layer |
Package |
Responsibility |
May import |
Must NOT import |
| Handler |
handler/ |
HTTP entry point; orchestrate business rules; validate and map input/output |
service/, model/, validation/ |
config/, DB drivers, repository/ |
| Service |
service/ |
Thin adapter over external APIs, queues, caches |
model/, config/ |
handler/ |
| Repository |
repository/ |
DB access only; returns domain objects |
model/, config/ |
handler/, service/ |
| Model |
model/ |
Pure data: domain structs + conversion methods (ToDTO, FromRow) |
nothing internal |
handler/, service/, repository/ |
| Validation |
validation/ |
Named validators for domain rules |
model/ |
handler/, service/ |
| Config |
config/ |
Load env/files; construct and wire concrete types |
all |
— |
Feature Implementation Checklist
Build in this order. Do not skip steps.
Step 1 — Model first (model/)
- Define new domain structs in
model/ (use model (singular) as the default package for domain models, unless there are more than 30 models, in which case they must be split into domain-specific packages).
- No behavior beyond conversion methods (
ToDTO(), FromRow()).
- Every exported field: snake_case JSON tag.
Step 2 — Repository / Service
- DB access →
repository/ package. One file per aggregate root.
- External API/queue →
service/ package. Retry + timeout logic only; no business decisions.
- Both MUST define an interface in the consumer package (see Interface Placement Rule below).
Step 3 — Handler (handler/)
- One handler struct per resource or domain area.
- Constructor:
func NewUserHandler(getter UserGetter, notifier NotifySender, log Logger) *UserHandler
- All business decisions live here. If a handler method exceeds ~100 lines, split into named helpers.
- Route registration: a separate
RegisterRoutes(r *gin.RouterGroup) method; never inline in main.go.
Step 4 — Validation (validation/)
- Every request struct gets a
Validate() error method.
- Validator names describe what they check:
ValidateCreateOrderRequest, not validate.
- Validation errors: return
validation.Error{Field: "...", Reason: "..."} — never raw strings.
Step 5 — Wiring (main.go or bootstrap.go)
- Concrete types only in
main.go/bootstrap.go.
- Inject interfaces everywhere else. (Exception: global state is acceptable/good for client, handler, and configuration if they are immutable).
- Configuration loading: Prefer
config.Default() from github.com/bizshuk/gosdk, falling back to raw viper manual setup only if the SDK is not supported/available.
- Initialization order: config → DB connection → repositories → services → handlers → router.
Interface Placement Rule (critical for testability)
Interfaces are defined where consumed, not where implemented.
// In handler/user.go — NOT in repository/user.go
type userGetter interface {
GetByID(ctx context.Context, id string) (*model.User, error)
}
This means handler tests can mock the interface without importing the repository package,
breaking the import cycle and enabling true unit isolation.
Constructor Injection Rules
| Dependency count |
Pattern |
| ≤ 4 deps |
Plain constructor params |
| 5+ deps |
HandlerOptions struct |
| Many optional knobs |
Functional options ...Option |
Error & Constant Conventions
- Wrap at every layer boundary:
fmt.Errorf("handler.GetUser: %w", err)
- Sentinel errors defined in the package that owns the concept:
var ErrNotFound = errors.New("not found")
- HTTP status mapping lives in the handler layer only — never map errors in service or repository
- Log once at the handler boundary; service and repository only wrap and return
- All constants must use
SCREAMING_SNAKE_CASE (e.g. MAX_RETRIES, DEFAULT_TIMEOUT).
Context Conventions
ctx context.Context is always the first parameter of any function that does I/O
- Never store
ctx in a struct
- Wrap every DB query and external call:
ctx, cancel := context.WithTimeout(ctx, cfg.DBTimeout); defer cancel()
- Timeouts come from config — never hardcode duration literals
Test Patterns
| Layer |
Test approach |
| Handler |
Mock all interfaces; use httptest.NewRecorder(); assert status + response body |
| Repository |
Use sqlmock or a real test DB via docker-compose; never mock the DB driver itself |
| Service |
Mock external HTTP/gRPC clients with interface mocks |
| Validation |
Table-driven tests required; cover valid, missing, and out-of-range inputs |
All test files: _test.go suffix, same package as the code under test (prefer white-box
tests for internal helpers, _test package suffix for public API contracts).
Source: BizShuk/gosdk — distributed by TomeVault.
1---2name: bizshuk-gosdk-golang-mvc3description: golang-mvc4---56# golang-mvc78Go MVC implementation conventions for **feature work and refactoring**.9This skill **guides code generation and actively enforces layer boundaries** —10it modifies and refactors code that violates MVC conventions.1112---1314## Layer Map1516| Layer | Package | Responsibility | May import | Must NOT import |17| ---------- | ------------- | --------------------------------------------------------------------------- | ----------------------------------- | ------------------------------------- |18| Handler | `handler/` | HTTP entry point; orchestrate business rules; validate and map input/output | `service/`, `model/`, `validation/` | `config/`, DB drivers, `repository/` |19| Service | `service/` | Thin adapter over external APIs, queues, caches | `model/`, `config/` | `handler/` |20| Repository | `repository/` | DB access only; returns domain objects | `model/`, `config/` | `handler/`, `service/` |21| Model | `model/` | Pure data: domain structs + conversion methods (`ToDTO`, `FromRow`) | nothing internal | `handler/`, `service/`, `repository/` |22| Validation | `validation/` | Named validators for domain rules | `model/` | `handler/`, `service/` |23| Config | `config/` | Load env/files; construct and wire concrete types | all | — |2425---2627## Feature Implementation Checklist2829Build in this order. Do not skip steps.3031### Step 1 — Model first (`model/`)3233- Define new domain structs in `model/` (use `model` (singular) as the default package for domain models, unless there are more than 30 models, in which case they must be split into domain-specific packages).34- No behavior beyond conversion methods (`ToDTO()`, `FromRow()`).35- Every exported field: snake_case JSON tag.3637### Step 2 — Repository / Service3839- DB access → `repository/` package. One file per aggregate root.40- External API/queue → `service/` package. Retry + timeout logic only; no business decisions.41- Both MUST define an interface in the **consumer** package (see Interface Placement Rule below).4243### Step 3 — Handler (`handler/`)4445- One handler struct per resource or domain area.46- Constructor: `func NewUserHandler(getter UserGetter, notifier NotifySender, log Logger) *UserHandler`47- All business decisions live here. If a handler method exceeds ~100 lines, split into named helpers.48- Route registration: a separate `RegisterRoutes(r *gin.RouterGroup)` method; never inline in `main.go`.4950### Step 4 — Validation (`validation/`)5152- Every request struct gets a `Validate() error` method.53- Validator names describe what they check: `ValidateCreateOrderRequest`, not `validate`.54- Validation errors: return `validation.Error{Field: "...", Reason: "..."}` — never raw strings.5556### Step 5 — Wiring (`main.go` or `bootstrap.go`)5758- Concrete types only in `main.go`/`bootstrap.go`.59- Inject interfaces everywhere else. (Exception: global state is acceptable/good for client, handler, and configuration if they are immutable).60- Configuration loading: Prefer `config.Default()` from `github.com/bizshuk/gosdk`, falling back to raw `viper` manual setup only if the SDK is not supported/available.61- Initialization order: config → DB connection → repositories → services → handlers → router.6263---6465## Interface Placement Rule (critical for testability)6667Interfaces are defined **where consumed**, not where implemented.6869```go70// In handler/user.go — NOT in repository/user.go71type userGetter interface {72 GetByID(ctx context.Context, id string) (*model.User, error)73}74```7576This means handler tests can mock the interface without importing the repository package,77breaking the import cycle and enabling true unit isolation.7879---8081## Constructor Injection Rules8283| Dependency count | Pattern |84| ------------------- | ------------------------------ |85| ≤ 4 deps | Plain constructor params |86| 5+ deps | `HandlerOptions` struct |87| Many optional knobs | Functional options `...Option` |8889---9091## Error & Constant Conventions9293- Wrap at every layer boundary: `fmt.Errorf("handler.GetUser: %w", err)`94- Sentinel errors defined in the package that owns the concept: `var ErrNotFound = errors.New("not found")`95- HTTP status mapping lives in the handler layer only — never map errors in service or repository96- Log once at the handler boundary; service and repository only wrap and return97- All constants must use `SCREAMING_SNAKE_CASE` (e.g. `MAX_RETRIES`, `DEFAULT_TIMEOUT`).9899---100101## Context Conventions102103- `ctx context.Context` is always the **first parameter** of any function that does I/O104- Never store `ctx` in a struct105- Wrap every DB query and external call: `ctx, cancel := context.WithTimeout(ctx, cfg.DBTimeout); defer cancel()`106- Timeouts come from config — never hardcode duration literals107108---109110## Test Patterns111112| Layer | Test approach |113| ---------- | ----------------------------------------------------------------------------------- |114| Handler | Mock all interfaces; use `httptest.NewRecorder()`; assert status + response body |115| Repository | Use `sqlmock` or a real test DB via docker-compose; never mock the DB driver itself |116| Service | Mock external HTTP/gRPC clients with interface mocks |117| Validation | Table-driven tests required; cover valid, missing, and out-of-range inputs |118119All test files: `_test.go` suffix, same package as the code under test (prefer white-box120tests for internal helpers, `_test` package suffix for public API contracts).121122---123> Source: [BizShuk/gosdk](https://github.com/BizShuk/gosdk) — distributed by [TomeVault](https://tomevault.io).124<!-- tomevault:4.0:skill_md:2026-06-15 -->