CoC — Convention over Configuration #

There are two ways to start a new project. The first: engineers must decide and configure every detail — file names, folder structure, naming formats, routing conventions, database mappings, how to inject dependencies — before writing a single line of business logic. The second: there are sensible defaults for all of that, and engineers only configure when there’s a real reason to deviate. Convention over Configuration (CoC) is the philosophy behind the second way. Its principle: systems should have sensible default conventions, so developers only make explicit decisions when the desired behavior differs from the convention. CoC isn’t about reducing flexibility — it’s about reducing unnecessary decisions. Every unnecessary decision made is time that could go to something more important. This article covers the CoC philosophy, how it works in Go and Dart, how to apply it in internal libraries and project structures, its concrete benefits for onboarding and team consistency, and when conventions should be violated.

What Is Convention over Configuration? #

CoC was popularized by Ruby on Rails, but the philosophy is much broader than one framework. The essence is:

If a class is named User, then its database table is users. If not explicitly declared, use the convention — don’t force developers to configure something that’s already obvious.

Rails made this famous, but the Go standard library, gRPC, protobuf, and many other modern tools all apply CoC quietly.

Examples of CoC in familiar ecosystems:

Tool / Framework      Default convention
───────────────────  ──────────────────────────────────────────────
Go testing           *_test.go files → automatically run by go test
                     Test*() functions → test cases
                     Benchmark*() functions → benchmarks
                     Example*() functions → verified examples

Go modules           go.mod at the root → this is the module root
                     internal/ → packages can't be imported from outside the module

protobuf / gRPC      message User → generates struct User in Go
                     field first_name → generates FirstName in Go (camelCase)
                     snake_case in proto → camelCase in generated code

GORM                 struct User → table users (automatic pluralization)
                     field ID → primary key
                     field CreatedAt → automatically filled on insert
                     field UpdatedAt → automatically filled on update

JSON encoding        field Name string → "Name" (without tags)
                     field Name string `json:"name"` → "name" (with tags, overriding the convention)

Kubernetes           deployment.yaml file → resource name from metadata.name
                     :latest image tag → IfNotPresent pull policy (default convention)

Docker               Dockerfile at the root → docker build . works directly
                     Last CMD → default entrypoint

The consistent pattern: conventions apply by default, configuration is used to deviate. Not the other way around.


The Problems CoC Solves #

Without clear conventions, every small decision becomes cognitive load that drains the team’s energy:

Without CoC — every detail needs an explicit decision:

  "Where should handler files go?"
  → 15-minute team discussion, and in the end each engineer has their own preference

  "What should the table for the Order struct be named?"
  → "orders", "order", "tbl_orders", "Order" — four engineers, four opinions

  "What JSON format for the created_at field?"
  → "created_at", "createdAt", "CreatedAt", "created" — inconsistent across endpoints

  "What should the user handler file be named?"
  → "user_handler.go", "handler_user.go", "users.go", "user.go"

  Result: an inconsistent codebase, slow onboarding,
  code reviews full of style debates instead of substance

With CoC — the convention exists, decisions are only for exceptions:

  "Where should handler files go?"
  → internal/api/ — project convention

  "What table name for the Order struct?"
  → orders — snake_case plural, convention already exists

  "What JSON format for created_at?"
  → snake_case — project API convention

  "What should the user handler file be named?"
  → user_handler.go — handler naming convention

  New engineers immediately know where to look and where to put things
flowchart TD
    Q{"Does this\ndeviate from\nthe convention?"}
    COC["Use the convention\n(no decision needed)"]
    CONFIG["Explicit configuration\n(with a clear reason)"]
    BENEFIT["CoC benefits:\n• Fewer decisions\n• Automatic consistency\n• Faster onboarding\n• Code reviews focus on substance"]

    Q -->|"No"| COC
    Q -->|"Yes, because of reason X"| CONFIG
    COC --> BENEFIT

    style COC fill:#5CB85C,color:#fff
    style BENEFIT fill:#4C9BE8,color:#fff

CoC in Go Naming and Code Structure #

Go itself is a consistent example of CoC. Conventions that don’t need configuring but are followed by the entire ecosystem:

// Go conventions that have become "unwritten law"

// 1. Package name = directory name (lowercase, one word)
// ✓ package user    → in directory internal/user/
// ✓ package pricing → in directory internal/pricing/
// ✗ package UserService → not a Go convention

// 2. Interface with one method → interface name = method + "er"
type Reader interface { Read(p []byte) (n int, err error) }
type Writer interface { Write(p []byte) (n int, err error) }
type Stringer interface { String() string }
// This convention makes interface naming predictable and consistent

// 3. Constructor → New + type name
func NewUserService(repo UserRepository) *UserService { ... }
func NewOrderHandler(svc *OrderService) *OrderHandler { ... }
// Anyone reading Go code knows that NewX is a constructor

// 4. Error variables → Err + condition name
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInvalidInput = errors.New("invalid input")
)
// This convention makes sentinel errors easy to find with grep

// 5. Unexported field → internal detail config, exported → public API
type UserService struct {
    repo      UserRepository // unexported = implementation detail
    notifier  Notifier       // unexported = swappable without breaking changes
    MaxRetries int           // exported = part of the public API (rare, but exists)
}

// 6. Test file → same name as the tested file, with _test suffix
// user_service.go → user_service_test.go
// order_handler.go → order_handler_test.go

// 7. Integration tests → _integration_test.go suffix or build tags
// //go:build integration

These conventions aren’t configured anywhere — they exist by default in every Go developer’s mind because they’re documented and reinforced by standard tools (gofmt, golint, go vet).


CoC in Project Structure #

A project structure following conventions lets new engineers immediately know where to find things — without needing separate documentation.

A structure following the Go community convention (not official, but widely adopted):

project/
  ├── cmd/
  │   └── api/
  │       └── main.go          ← application entry point
  ├── internal/                ← code that must not be imported from outside the module
  │   ├── domain/              ← entities and business rules (knows no infrastructure)
  │   │   ├── user/
  │   │   │   ├── user.go      ← User struct, Status, domain methods
  │   │   │   └── errors.go    ← ErrNotFound, ErrUnauthorized, etc.
  │   │   └── order/
  │   │       ├── order.go
  │   │       └── errors.go
  │   ├── service/             ← business logic, orchestration
  │   │   ├── user_service.go
  │   │   └── order_service.go
  │   ├── repository/          ← data access, domain interface implementations
  │   │   ├── postgres/
  │   │   │   ├── user_repo.go
  │   │   │   └── order_repo.go
  │   │   └── redis/
  │   │       └── session_repo.go
  │   └── api/                 ← HTTP handlers, middleware, router
  │       ├── handler/
  │       │   ├── user_handler.go
  │       │   └── order_handler.go
  │       ├── middleware/
  │       │   ├── auth.go
  │       │   └── logging.go
  │       └── router.go
  ├── pkg/                     ← code importable from outside (if any)
  │   └── validator/
  │       └── email.go
  ├── migrations/              ← database migration files
  │   ├── 001_create_users.sql
  │   └── 002_create_orders.sql
  ├── config/
  │   └── config.go
  ├── go.mod
  └── go.sum

Conventions in effect in this structure:
  - Directory names reflect the layer/concern
  - Handler files: {domain}_handler.go
  - Service files: {domain}_service.go
  - Repository files: {domain}_repo.go
  - Migration files: {sequence}_{description}.sql

New engineers immediately know:
  "Where is the user handler?" → internal/api/handler/user_handler.go
  "Where is the order business logic?" → internal/service/order_service.go
  "Where are the database migrations?" → migrations/

CoC in Databases and ORMs #

Consistent database conventions eliminate recurring decisions about table, column, and index naming.

// Database naming conventions applied consistently:
//
// Struct → Table:
//   User         → users          (snake_case, plural)
//   OrderItem    → order_items    (snake_case, plural)
//   UserProfile  → user_profiles
//
// Field → Column:
//   ID           → id             (primary key)
//   UserID       → user_id        (foreign key)
//   CreatedAt    → created_at
//   UpdatedAt    → updated_at
//   DeletedAt    → deleted_at     (for soft deletes)
//   FirstName    → first_name
//
// Indexes:
//   Primary key  → idx_{table}_pkey (automatic)
//   Foreign key  → idx_{table}_{column}_fkey
//   Unique       → uniq_{table}_{column}
//   Search       → idx_{table}_{column}

// With GORM — these conventions are built in, no explicit config needed:
type User struct {
    ID        uint           `gorm:"primarykey"`
    Name      string
    Email     string         `gorm:"uniqueIndex"`
    CreatedAt time.Time      // automatically filled on insert
    UpdatedAt time.Time      // automatically filled on update
    DeletedAt gorm.DeletedAt `gorm:"index"` // automatic soft delete
}
// Table: users — GORM pluralization convention

type Order struct {
    ID        uint
    UserID    uint      // GORM knows this is a foreign key to users.id
    User      User      // belongs to — automatic from the UserID field
    Total     float64
    Status    string
    CreatedAt time.Time
    UpdatedAt time.Time
}
// Table: orders

// Without GORM — apply the same conventions manually and consistently:
const (
    // Convention: queries use $N named parameters for PostgreSQL
    queryFindUserByEmail = `
        SELECT id, name, email, created_at, updated_at
        FROM users
        WHERE email = $1 AND deleted_at IS NULL
    `
    queryInsertUser = `
        INSERT INTO users (name, email, password_hash, created_at, updated_at)
        VALUES ($1, $2, $3, NOW(), NOW())
        RETURNING id, created_at, updated_at
    `
    queryFindOrdersByUserID = `
        SELECT id, user_id, total, status, created_at
        FROM orders
        WHERE user_id = $1
        ORDER BY created_at DESC
    `
    // Convention: query constants named query{Operation}{Entity}By{Condition}
)

CoC in API Design #

Consistent API conventions make endpoints predictable — clients don’t need to read documentation to guess the right URL and method.

REST conventions that have become the de facto standard:

Resource    HTTP Method   URL                        Description
──────────  ────────────  ─────────────────────────  ─────────────────────
users       GET           /api/v1/users              List all users
            POST          /api/v1/users              Create a new user
            GET           /api/v1/users/:id          Get a user by ID
            PUT           /api/v1/users/:id          Update a user (full replace)
            PATCH         /api/v1/users/:id          Update a user (partial)
            DELETE        /api/v1/users/:id          Delete a user

orders      GET           /api/v1/orders             List orders
            POST          /api/v1/orders             Create a new order
            GET           /api/v1/orders/:id         Get an order by ID
            PATCH         /api/v1/orders/:id/status  Update status (nested action)

Consistent response conventions:

Success (200):          Error (4xx/5xx):
{                       {
  "data": { ... },        "code": "USER_NOT_FOUND",
  "meta": {               "message": "user not found",
    "page": 1,            "trace_id": "abc-123"
    "per_page": 10,     }
    "total": 100
  }
}

Versioning conventions:
  /api/v1/ → current version
  /api/v2/ → new breaking version (v1 keeps running during the deprecation period)

Pagination conventions:
  GET /api/v1/users?page=1&per_page=20
  → page and per_page always exist as query parameters
  → defaults: page=1, per_page=20, max per_page=100
// Implementing consistent response conventions
type SuccessResponse struct {
    Data interface{} `json:"data"`
    Meta *Meta       `json:"meta,omitempty"`
}

type Meta struct {
    Page    int `json:"page"`
    PerPage int `json:"per_page"`
    Total   int `json:"total"`
}

type ErrorResponse struct {
    Code    string `json:"code"`
    Message string `json:"message"`
    TraceID string `json:"trace_id,omitempty"`
}

// Helpers that enforce response conventions — all handlers use these
func writeSuccess(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(SuccessResponse{Data: data})
}

func writeSuccessList(w http.ResponseWriter, data interface{}, meta Meta) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(SuccessResponse{Data: data, Meta: &meta})
}

func writeError(w http.ResponseWriter, status int, code, message, traceID string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(ErrorResponse{
        Code:    code,
        Message: message,
        TraceID: traceID,
    })
}

// A handler following the convention — consistent with every other handler
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
    p := parsePagination(r) // convention: parsePagination always exists
    users, total, err := h.service.ListUsers(r.Context(), p)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "INTERNAL_ERROR",
            "failed to retrieve users", traceIDFrom(r))
        return
    }
    writeSuccessList(w, users, Meta{Page: p.Page, PerPage: p.Limit, Total: total})
}

Building CoC into Internal Libraries #

When a team is large enough, building a small internal library or framework that enforces conventions is a highly profitable investment. New engineers can become productive faster because they don’t need to make decisions about things that already have conventions.

// Example: a small internal HTTP framework enforcing response conventions

// pkg/httpkit/handler.go
package httpkit

import (
    "context"
    "encoding/json"
    "net/http"
)

// Handler is the convention for all handlers in this project
// Returning (interface{}, error) instead of writing directly to http.ResponseWriter
// This convention consistently separates business logic from HTTP concerns
type Handler func(ctx context.Context, r *http.Request) (interface{}, error)

// Adapt converts a Handler into an http.HandlerFunc following response conventions
func Adapt(h Handler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        traceID := traceIDFromContext(r.Context())

        result, err := h(r.Context(), r)
        if err != nil {
            handleError(w, err, traceID)
            return
        }

        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(SuccessResponse{Data: result})
    }
}

func handleError(w http.ResponseWriter, err error, traceID string) {
    // Convention: all domain errors are translated to consistent HTTP statuses
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        writeErrorResponse(w, apiErr.HTTPStatus, apiErr.Code, apiErr.Message, traceID)
        return
    }

    // Log the internal error, expose only a generic message to the client
    slog.Error("unhandled error", "trace_id", traceID, "error", err)
    writeErrorResponse(w, http.StatusInternalServerError,
        "INTERNAL_ERROR", "an unexpected error occurred", traceID)
}

// Usage: handlers follow the convention without needing HTTP details
func (h *UserHandler) GetUser(ctx context.Context, r *http.Request) (interface{}, error) {
    id := chi.URLParam(r, "id") // or r.PathValue("id") in Go 1.22+
    if id == "" {
        return nil, NewAPIError(http.StatusBadRequest, "MISSING_ID", "user id is required")
    }

    user, err := h.service.GetUser(ctx, id)
    if err != nil {
        return nil, err // Adapt() will handle the HTTP translation
    }
    return user, nil
}

// Router setup follows the convention
router.Get("/api/v1/users/{id}", httpkit.Adapt(userHandler.GetUser))
// Another example: an internal repository convention
// Every repository follows the same interface

// pkg/repository/base.go
package repository

import "context"

// Convention: all repositories implement this interface for basic CRUD operations
type Base[T any] interface {
    FindByID(ctx context.Context, id string) (*T, error)
    Save(ctx context.Context, entity T) error
    Update(ctx context.Context, entity T) error
    Delete(ctx context.Context, id string) error
    List(ctx context.Context, filter Filter) ([]T, int, error)
}

type Filter struct {
    Page    int
    PerPage int
    // Other fields are added per repository with embedding or composition
}

// A repository following the convention is immediately understood
// by any engineer already familiar with this project
type UserRepository struct {
    db *sql.DB
}

// Because it follows the Base[User] convention, engineers immediately know
// which methods are available without opening the repository file

CoC in Flutter/Dart #

In Flutter, CoC is most felt in project structure and file naming. Dart conventions enforced by dart format and dart analyze:


// 1. File names: snake_case
// user_service.go, order_repository.go, payment_gateway.go

// 2. Type names: PascalCase (exported) or lowerCamelCase (unexported)
type UserService struct{}
type OrderRepository struct{}
type PaymentGateway struct{}

// 3. Variable and function names: mixedCaps
var userService = UserService{}

func createOrder() {}

// 4. Constants: mixedCaps (Go convention — no UPPER_SNAKE)
const maxRetryCount = 3
const defaultPageSize = 20

// 5. Private: lowercase first letter; public: uppercase
type UserService struct {
    repository UserRepository // private — Go convention
    logger     Logger
}

// A layered Go project structure following conventions:
project/
  ├── cmd/
  │   └── api/
  │       └── main.go              ← entry point
  ├── internal/                    ← code that must not be imported from outside
  │   ├── core/                    ← shared utilities, knows no domain
  │   │   ├── network/
  │   │   │   └── api_client.go
  │   │   ├── error/
  │   │   │   └── exceptions.go
  │   │   └── utils/
  │   │       └── validators.go
  │   ├── features/                ← features organized per domain
  │   │   ├── auth/
  │   │   │   ├── data/
  │   │   │   │   ├── auth_repository_impl.go
  │   │   │   │   └── auth_remote_source.go
  │   │   │   ├── domain/
  │   │   │   │   ├── auth_repository.go  ← interface
  │   │   │   │   └── user_entity.go
  │   │   │   └── presentation/
  │   │   │       ├── auth_screen.go
  │   │   │       └── auth_view_model.go
  │   │   └── order/
  │   │       ├── data/
  │   │       ├── domain/
  │   │       └── presentation/
  │   └── shared/                  ← components used by many features
  │       ├── widgets/
  │       │   ├── loading_indicator.go
  │       │   └── error_view.go
  │       └── theme/
  │           └── app_theme.go
  ├── go.mod

// Go naming conventions:
// Screen (full page): UserProfileScreen, OrderListScreen
// Widget (component): UserAvatar, OrderCard, PriceTag
// ViewModel / Controller: UserProfileViewModel, OrderListController
// Repository interface: UserRepository (without Impl)
// Repository implementation: UserRepositoryImpl
// Go conventions that have become the standard:

// 1. File names: snake_case
// user_service.go, order_repository.go, payment_gateway.go

// 2. Type names: PascalCase (exported) or lowerCamelCase (unexported)
type UserService struct{}
type OrderRepository struct{}
type PaymentGateway struct{}

// 3. Variable and function names: mixedCaps
var userService = UserService{}

func createOrder() {}

// 4. Constants: mixedCaps (Go convention — no UPPER_SNAKE)
const maxRetryCount = 3
const defaultPageSize = 20

// 5. Private: lowercase first letter; public: uppercase
type UserService struct {
    repository UserRepository // private — Go convention
    logger     Logger
}

// A layered Go project structure following conventions:
// project/
//   ├── cmd/
//   │   └── api/
//   │       └── main.go              ← entry point
//   ├── internal/                    ← code that must not be imported from outside
//   │   ├── core/                    ← shared utilities, knows no domain
//   │   │   ├── network/
//   │   │   │   └── api_client.go
//   │   │   ├── error/
//   │   │   │   └── exceptions.go
//   │   │   └── utils/
//   │   │       └── validators.go
//   │   ├── features/                ← features organized per domain
//   │   │   ├── auth/
//   │   │   │   ├── data/
//   │   │   │   │   ├── auth_repository_impl.go
//   │   │   │   │   └── auth_remote_source.go
//   │   │   │   ├── domain/
//   │   │   │   │   ├── auth_repository.go  ← interface
//   │   │   │   │   └── user_entity.go
//   │   │   │   └── presentation/
//   │   │   │       ├── auth_screen.go
//   │   │   │       └── auth_view_model.go
//   │   │   └── order/
//   │   │       ├── data/
//   │   │       ├── domain/
//   │   │       └── presentation/
//   │   └── shared/                  ← components used by many features
//   │       ├── widgets/
//   │       │   ├── loading_indicator.go
//   │       │   └── error_view.go
//   │       └── theme/
//   │           └── app_theme.go
//   ├── go.mod

// Go naming conventions:
// Screen (full page): UserProfileScreen, OrderListScreen
// Widget (component): UserAvatar, OrderCard, PriceTag
// ViewModel / Controller: UserProfileViewModel, OrderListController
// Repository interface: UserRepository (without Impl)
// Repository implementation: UserRepositoryImpl

When Conventions Should Be Violated #

CoC isn’t dogma. There are situations where deviating from a convention is the right decision — as long as the deviation is deliberate, documented, and has a clear reason.

JUSTIFIED DEVIATIONS FROM CONVENTIONS:

  1. Performance-critical code
     ORM conventions (GORM) produce suboptimal queries for
     complex reports → write optimized raw SQL
     → Document: "Raw SQL is used here because the ORM can't
       generate an efficient query plan for this aggregation"

  2. Legacy system integration
     Legacy database tables have inconsistent names (tbl_usr, USER_DATA)
     → Keep following conventions in Go code, map to legacy names in the repository
     → Don't let legacy names leak into the domain model

  3. Specific technical requirements
     Endpoints with non-standard behavior because of payment gateway
     integration with special webhook requirements
     → Document why this endpoint differs from normal REST conventions

  4. Domains with their own conventions
     Finance/accounting has its own conventions (debit/credit, journals, ledgers)
     → Follow domain conventions, not generic technical ones

UNJUSTIFIED DEVIATIONS:
  ✗ "I prefer this name" — personal preference isn't a sufficient reason
  ✗ "In my old project we did it this way" — consistency with THIS project matters more
  ✗ "It's more flexible" — without a concrete use case needing that flexibility
  ✗ No explainable reason — the convention must be followed

A rule of thumb: if a deviation from the convention can't be explained
in one concrete sentence, don't do it.
CoC is most effective when the conventions are documented. Conventions living only in senior engineers’ heads — but never written down — are CoC that will crumble as the team turns over. Document conventions in an ADR (Architecture Decision Record), README, or CONTRIBUTING.md. Documented conventions can be enforced in code review; undocumented ones can only be enforced by whoever remembers them.

Documenting and Enforcing Conventions #

Conventions that aren’t enforced will be violated. There are several ways to ensure conventions are followed consistently:

1. AUTOMATED TOOLING (most effective):
   gofmt / goimports      → consistently formats code
   golangci-lint          → enforces naming conventions, error handling patterns
   dart format            → consistently formats Dart
   dart analyze           → static analysis with custom lint rules
   Custom linters         → can be built for project-specific conventions

2. TEMPLATES AND GENERATORS:
   go generate            → generates boilerplate following conventions
   Template files         → starter templates for new handlers, services, repositories
   Makefile targets       → "make new-handler NAME=user" → generates files with conventions

3. CODE REVIEW CHECKLISTS:
   Do file names follow the {domain}_{layer}.go convention?
   Do response shapes use the writeSuccess/writeError helpers?
   Are errors wrapped with fmt.Errorf("functionName: %w", err)?
   Do test files sit next to the files they test?

4. DOCUMENTATION:
   CONTRIBUTING.md        → conventions contributors must follow
   ADR (Architecture Decision Record) → why this convention was chosen
   Per-directory READMEs  → explaining directory structure conventions
# A Makefile that helps enforce conventions
.PHONY: new-handler new-service lint test

# Generate a new handler following the convention
new-handler:
	@if [ -z "$(NAME)" ]; then echo "Usage: make new-handler NAME=user"; exit 1; fi
	@cp templates/handler.go.tmpl internal/api/handler/$(NAME)_handler.go
	@cp templates/handler_test.go.tmpl internal/api/handler/$(NAME)_handler_test.go
	@sed -i 's/{{NAME}}/$(NAME)/g' internal/api/handler/$(NAME)_handler.go
	@echo "Created internal/api/handler/$(NAME)_handler.go"

# Enforce conventions with linters
lint:
	golangci-lint run ./...
	go vet ./...

# Run all tests following Go testing conventions
test:
	go test -race -cover ./...

# Run only integration tests
test-integration:
	go test -tags=integration -race ./...

CoC’s Relationship with Other Principles #

flowchart TD
    COC["CoC\n(Convention over Configuration)"]

    DRY2["DRY\nConventions are DRY\nat the decision level —\nthe same decision doesn't\nneed to be made repeatedly"]
    KISS2["KISS\nSmart defaults\neliminate unnecessary\ncomplexity"]
    YAGNI2["YAGNI\nConfiguration only\nwhen there's a real reason\nto deviate"]
    SRP2["SRP\nEvery layer has a clear\nconvention about what\nbelongs inside it"]

    COC -->|"is DRY at the\ndesign decision level"| DRY2
    COC -->|"produces"| KISS2
    COC -->|"synergizes with"| YAGNI2
    COC -->|"reinforced by"| SRP2

    style COC fill:#4C9BE8,color:#fff
    style DRY2 fill:#5CB85C,color:#fff
    style KISS2 fill:#5CB85C,color:#fff
    style YAGNI2 fill:#5CB85C,color:#fff
    style SRP2 fill:#5CB85C,color:#fff

CoC is DRY at the design decision level: instead of duplicating the same decision in every place that needs it, a convention makes that decision once and applies it everywhere. YAGNI reinforces CoC from the opposite direction: don’t create configuration for something where the default convention is already sufficient.


Anti-Patterns at a Glance #

// ✗ Explicit configuration for something that already has a convention
type UserRepository struct {
    db        *sql.DB
    tableName string // "users" — why configure it? the convention is clear
    idField   string // "id" — already obvious from the convention
}

// ✗ Naming that doesn't follow Go conventions without a reason
type userservicehandler struct{}  // should be UserServiceHandler or split up
func (h *userservicehandler) doCreateUser() {} // should be CreateUser

// ✗ Response shapes inconsistent across endpoints
// GET /users → {"users": [...], "count": 10}
// GET /orders → {"data": [...], "total": 10, "page": 1}
// GET /products → [...]  (direct array, no wrapper)

// ✗ Folder structures that don't reflect layer conventions
internal/
  stuff/        // "stuff" isn't a meaningful layer name
  things/       // same
  misc/         // a util package under a different name

// ✗ Inconsistent error formats
return fmt.Errorf("error: %v", err)           // no function context
return errors.New("something went wrong")      // not specific
return fmt.Errorf("UserService.Create: %v", err) // function name, not "createUser"

// ✓ A consistent error convention:
return fmt.Errorf("createUser %s: save to db: %w", email, err)

// ✗ Tests not following Go conventions
func testCreateUser(t *testing.T) {} // lowercase — go test won't run it!
// ✓ func TestCreateUser(t *testing.T) {}

CoC Review Checklist #

NAMING:
  □ Package names lowercase, one word, matching the directory name
  □ One-method interfaces → name = method + "er" (Reader, Writer, Stringer)
  □ Constructors → New + type name (NewUserService, NewOrderHandler)
  □ Error variables → Err + condition name (ErrNotFound, ErrUnauthorized)
  □ Test files → {filename}_test.go in the same directory

PROJECT STRUCTURE:
  □ Directories reflect clear layers/concerns (domain, service, repository, api)
  □ No "util", "misc", "stuff", "things" directories
  □ Migration files → numeric sequence + description (001_create_users.sql)

API AND RESPONSES:
  □ URLs follow REST conventions (plural nouns, nested for relationships)
  □ Success responses use a consistent shape across all endpoints
  □ Error responses use a consistent shape (code, message, trace_id)
  □ Pagination parameters consistent (page, per_page) across all list endpoints

DATABASE:
  □ Table names: snake_case, plural (users, order_items)
  □ Column names: snake_case (user_id, created_at)
  □ Primary keys always id
  □ Timestamps always created_at and updated_at

DOCUMENTED CONVENTIONS:
  □ Project conventions live in CONTRIBUTING.md or an ADR
  □ Deviations from conventions have documented reasons
  □ Linters or automated tools used to enforce conventions

Summary #

  • CoC means systems have sensible defaults — developers only make explicit decisions when they want to deviate. Not reducing flexibility, but reducing unnecessary decisions.
  • The problems it solves: without conventions, every small detail becomes a time-draining discussion. With conventions, new engineers immediately know where to look and where to put things — without separate documentation.
  • Go already has built-in CoC: package naming, NewX constructors, Xer interfaces, ErrX errors, *_test.go files, internal/ directories — all conventions understood by the entire ecosystem.
  • Project structure as CoC: consistent directory structures (domain/, service/, repository/, api/) make every component’s location predictable and reduce navigation overhead.
  • APIs as CoC: consistent URLs, HTTP methods, response shapes, and pagination make endpoints predictable — clients don’t need to read documentation to guess the right format.
  • Internal libraries as enforcers: small wrappers for HTTP responses, base repository interfaces, or code generators are how teams enforce conventions automatically, not by relying on individual discipline.
  • CoC needs documentation: conventions living only in senior engineers’ heads will crumble as the team turns over. Write them in CONTRIBUTING.md, ADRs, or per-directory READMEs.
  • Tooling is the best enforcer: gofmt, golangci-lint, dart format, and custom linters enforce conventions without relying on manual code review.
  • Deviations must be deliberate and documented: personal preference isn’t a sufficient reason. Valid deviations have concrete technical reasons explainable in one sentence.
  • Relationship with other principles: CoC is DRY at the design decision level, synergizes with YAGNI (configure only when there’s a real reason), and is reinforced by SRP (every layer has clear conventions).

← Previous: Fail Fast   Next: Principle of Least Privilege →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact