CoC — Convention over Configuration #

Ada dua cara untuk memulai project baru. Cara pertama: engineer harus memutuskan dan mengonfigurasi setiap detail — nama file, struktur folder, format penamaan, konvensi routing, pemetaan database, cara inject dependency — sebelum bisa menulis satu baris business logic pun. Cara kedua: ada default yang masuk akal untuk semua itu, dan engineer hanya perlu mengonfigurasi ketika memang ada alasan untuk menyimpang. Convention over Configuration (CoC) adalah filosofi di balik cara kedua. Prinsipnya: sistem sebaiknya punya konvensi default yang masuk akal, sehingga developer hanya perlu membuat keputusan eksplisit ketika perilaku yang diinginkan berbeda dari konvensi. CoC bukan tentang mengurangi fleksibilitas — ia tentang mengurangi keputusan yang tidak perlu. Setiap keputusan yang tidak perlu dibuat adalah waktu yang bisa digunakan untuk hal yang lebih penting. Artikel ini membahas filosofi CoC, bagaimana ia bekerja di Go dan Dart, cara menerapkannya di internal library dan project structure, keuntungan konkretnya untuk onboarding dan konsistensi tim, serta kapan konvensi justru harus dilanggar.

Apa Itu Convention over Configuration? #

CoC dipopulerkan oleh Ruby on Rails, tapi filosofinya jauh lebih luas dari satu framework. Intinya adalah:

Jika nama sebuah class adalah User, maka tabel database-nya adalah users. Jika tidak dideklarasikan secara eksplisit, gunakan konvensi — jangan paksa developer mengonfigurasi sesuatu yang sudah jelas.

Rails menjadikan ini terkenal, tapi Go standard library, gRPC, protobuf, dan banyak tool modern lainnya semuanya menerapkan CoC secara diam-diam.

Contoh CoC di ekosistem yang sudah familiar:

Tool / Framework     Konvensi default
───────────────────  ──────────────────────────────────────────────
Go testing           File *_test.go → otomatis dijalankan saat go test
                     Fungsi Test*() → test case
                     Fungsi Benchmark*() → benchmark
                     Fungsi Example*() → contoh yang diverifikasi

Go modules           go.mod di root → ini adalah module root
                     internal/ → package tidak bisa diimpor dari luar module

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

GORM                 struct User → tabel users (pluralisasi otomatis)
                     field ID → primary key
                     field CreatedAt → otomatis diisi saat insert
                     field UpdatedAt → otomatis diisi saat update

JSON encoding        field Name string → "Name" (tanpa tag)
                     field Name string `json:"name"` → "name" (dengan tag, override konvensi)

Kubernetes           file deployment.yaml → nama resource dari field metadata.name
                     image tag :latest → pull policy IfNotPresent (konvensi default)

Docker               Dockerfile di root → docker build . langsung bekerja
                     CMD terakhir → entrypoint default

Pola yang konsisten: konvensi berlaku secara default, konfigurasi digunakan untuk menyimpang. Bukan sebaliknya.


Masalah yang Diselesaikan CoC #

Tanpa konvensi yang jelas, setiap keputusan kecil menjadi beban kognitif yang menguras energi tim:

Tanpa CoC — setiap hal perlu keputusan eksplisit:

  "Di mana file handler diletakkan?"
  → Diskusi tim 15 menit, akhirnya masing-masing engineer punya preferensi berbeda

  "Apa nama tabel untuk struct Order?"
  → "orders", "order", "tbl_orders", "Order" — empat engineer empat pendapat

  "Bagaimana format JSON untuk field created_at?"
  → "created_at", "createdAt", "CreatedAt", "created" — tidak konsisten antar endpoint

  "Nama file untuk handler user?"
  → "user_handler.go", "handler_user.go", "users.go", "user.go"

  Hasilnya: codebase yang tidak konsisten, onboarding yang lama,
  code review yang penuh diskusi tentang style bukan substance

Dengan CoC — konvensi sudah ada, keputusan hanya untuk exception:

  "Di mana file handler diletakkan?"
  → internal/api/ — konvensi project

  "Nama tabel untuk struct Order?"
  → orders — snake_case plural, sudah ada konvensi

  "Format JSON untuk created_at?"
  → snake_case — konvensi API project

  "Nama file untuk handler user?"
  → user_handler.go — konvensi penamaan handler

  Engineer baru langsung tahu di mana mencari dan di mana meletakkan sesuatu
flowchart TD
    Q{"Apakah ini\nmenyimpang dari\nkonvensi?"}
    COC["Gunakan konvensi\n(tidak perlu keputusan)"]
    CONFIG["Konfigurasi eksplisit\n(dengan alasan yang jelas)"]
    BENEFIT["Keuntungan CoC:\n• Lebih sedikit keputusan\n• Konsistensi otomatis\n• Onboarding lebih cepat\n• Code review fokus ke substance"]

    Q -->|"Tidak"| COC
    Q -->|"Ya, karena alasan X"| CONFIG
    COC --> BENEFIT

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

CoC di Penamaan dan Struktur Kode Go #

Go sendiri adalah contoh CoC yang konsisten. Konvensi yang tidak perlu dikonfigurasi tapi diikuti oleh seluruh ekosistem:

// Konvensi Go yang sudah menjadi "hukum tidak tertulis"

// 1. Nama package = nama direktori (lowercase, satu kata)
// ✓ package user    → di direktori internal/user/
// ✓ package pricing → di direktori internal/pricing/
// ✗ package UserService → bukan konvensi Go

// 2. Interface berisi satu method → nama interface = 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 }
// Konvensi ini membuat interface naming predictable dan consistent

// 3. Constructor → New + nama type
func NewUserService(repo UserRepository) *UserService { ... }
func NewOrderHandler(svc *OrderService) *OrderHandler { ... }
// Siapapun yang membaca kode Go tahu bahwa NewX adalah constructor

// 4. Error variables → Err + nama kondisi
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrInvalidInput = errors.New("invalid input")
)
// Konvensi ini membuat error sentinel mudah dicari dengan grep

// 5. Unexported field → konfigurasi detail internal, exported → public API
type UserService struct {
    repo      UserRepository // unexported = detail implementasi
    notifier  Notifier       // unexported = bisa diganti tanpa breaking change
    MaxRetries int           // exported = bagian dari public API (jarang, tapi ada)
}

// 6. Test file → sama nama dengan file yang ditest, suffix _test
// user_service.go → user_service_test.go
// order_handler.go → order_handler_test.go

// 7. Integration test → suffix _integration_test.go atau build tag
// //go:build integration

Konvensi-konvensi ini tidak dikonfigurasi di mana-mana — mereka ada secara default di pikiran setiap Go developer karena didokumentasikan dan diperkuat oleh tool standar (gofmt, golint, go vet).


CoC di Project Structure #

Struktur project yang mengikuti konvensi memungkinkan engineer baru langsung tahu di mana mencari sesuatu — tanpa perlu dokumentasi terpisah.

Struktur yang mengikuti konvensi Go community (bukan official, tapi sangat widely adopted):

project/
  ├── cmd/
  │   └── api/
  │       └── main.go          ← entry point aplikasi
  ├── internal/                ← kode yang tidak boleh diimpor dari luar module
  │   ├── domain/              ← entity dan business rules (tidak tahu infrastruktur)
  │   │   ├── user/
  │   │   │   ├── user.go      ← User struct, Status, domain methods
  │   │   │   └── errors.go    ← ErrNotFound, ErrUnauthorized, dll
  │   │   └── order/
  │   │       ├── order.go
  │   │       └── errors.go
  │   ├── service/             ← business logic, orchestration
  │   │   ├── user_service.go
  │   │   └── order_service.go
  │   ├── repository/          ← data access, implementasi interface domain
  │   │   ├── 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/                     ← kode yang boleh diimpor dari luar (jika ada)
  │   └── validator/
  │       └── email.go
  ├── migrations/              ← database migration files
  │   ├── 001_create_users.sql
  │   └── 002_create_orders.sql
  ├── config/
  │   └── config.go
  ├── go.mod
  └── go.sum

Konvensi yang berlaku di struktur ini:
  - Nama direktori mencerminkan layer/concern
  - Handler file: {domain}_handler.go
  - Service file: {domain}_service.go
  - Repository file: {domain}_repo.go
  - Migration file: {urutan}_{deskripsi}.sql

Engineer baru langsung tahu:
  "Handler user ada di mana?" → internal/api/handler/user_handler.go
  "Business logic order ada di mana?" → internal/service/order_service.go
  "Migrasi database di mana?" → migrations/

CoC di Database dan ORM #

Konvensi database yang konsisten menghilangkan keputusan berulang tentang penamaan tabel, kolom, dan index.

// Konvensi penamaan database yang diterapkan secara konsisten:
//
// Struct → Tabel:
//   User         → users          (snake_case, plural)
//   OrderItem    → order_items    (snake_case, plural)
//   UserProfile  → user_profiles
//
// Field → Kolom:
//   ID           → id             (primary key)
//   UserID       → user_id        (foreign key)
//   CreatedAt    → created_at
//   UpdatedAt    → updated_at
//   DeletedAt    → deleted_at     (untuk soft delete)
//   FirstName    → first_name
//
// Index:
//   Primary key  → idx_{table}_pkey (otomatis)
//   Foreign key  → idx_{table}_{column}_fkey
//   Unique       → uniq_{table}_{column}
//   Search       → idx_{table}_{column}

// Dengan GORM — konvensi ini sudah built-in, tidak perlu konfigurasi eksplisit:
type User struct {
    ID        uint           `gorm:"primarykey"`
    Name      string
    Email     string         `gorm:"uniqueIndex"`
    CreatedAt time.Time      // otomatis diisi saat insert
    UpdatedAt time.Time      // otomatis diisi saat update
    DeletedAt gorm.DeletedAt `gorm:"index"` // soft delete otomatis
}
// Tabel: users — konvensi GORM pluralisasi

type Order struct {
    ID        uint
    UserID    uint      // GORM tahu ini adalah foreign key ke users.id
    User      User      // belongs to — otomatis dari field UserID
    Total     float64
    Status    string
    CreatedAt time.Time
    UpdatedAt time.Time
}
// Tabel: orders

// Tanpa GORM — terapkan konvensi yang sama secara manual dan konsisten:
const (
    // Konvensi: query dengan named parameter $N untuk 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
    `
    // Konvensi: query constant diberi nama query{Operasi}{Entity}By{Kondisi}
)

CoC di API Design #

Konvensi API yang konsisten membuat endpoint predictable — client tidak perlu membaca dokumentasi untuk menebak URL dan method yang tepat.

Konvensi REST yang menjadi standar de facto:

Resource    HTTP Method   URL                        Deskripsi
──────────  ────────────  ─────────────────────────  ─────────────────────
users       GET           /api/v1/users              List semua user
            POST          /api/v1/users              Buat user baru
            GET           /api/v1/users/:id          Ambil user by ID
            PUT           /api/v1/users/:id          Update user (full replace)
            PATCH         /api/v1/users/:id          Update user (partial)
            DELETE        /api/v1/users/:id          Hapus user

orders      GET           /api/v1/orders             List order
            POST          /api/v1/orders             Buat order baru
            GET           /api/v1/orders/:id         Ambil order by ID
            PATCH         /api/v1/orders/:id/status  Update status (nested action)

Konvensi response yang konsisten:

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
  }
}

Konvensi versioning:
  /api/v1/ → versi saat ini
  /api/v2/ → versi baru yang breaking (v1 tetap jalan selama deprecation period)

Konvensi pagination:
  GET /api/v1/users?page=1&per_page=20
  → selalu ada page dan per_page sebagai query parameter
  → default: page=1, per_page=20, max per_page=100
// Implementasi konvensi response yang konsisten
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"`
}

// Helper yang menegakkan konvensi response — semua handler gunakan ini
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,
    })
}

// Handler yang mengikuti konvensi — konsisten dengan semua handler lainnya
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
    p := parsePagination(r) // konvensi: selalu ada parsePagination
    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})
}

Membuat CoC di Internal Library #

Ketika tim cukup besar, membuat internal library atau framework kecil yang menegakkan konvensi adalah investasi yang sangat menguntungkan. Engineer baru bisa produktif lebih cepat karena mereka tidak perlu membuat keputusan tentang hal-hal yang sudah punya konvensi.

// Contoh: internal HTTP framework kecil yang menegakkan konvensi response

// pkg/httpkit/handler.go
package httpkit

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

// Handler adalah konvensi untuk semua handler di project ini
// Mengembalikan (interface{}, error) bukan langsung menulis ke http.ResponseWriter
// Konvensi ini memisahkan business logic dari HTTP concern secara konsisten
type Handler func(ctx context.Context, r *http.Request) (interface{}, error)

// Adapt mengubah Handler ke http.HandlerFunc mengikuti konvensi response
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) {
    // Konvensi: semua error domain diterjemahkan ke HTTP status yang konsisten
    var apiErr *APIError
    if errors.As(err, &apiErr) {
        writeErrorResponse(w, apiErr.HTTPStatus, apiErr.Code, apiErr.Message, traceID)
        return
    }

    // Log internal error, expose hanya pesan generik ke client
    slog.Error("unhandled error", "trace_id", traceID, "error", err)
    writeErrorResponse(w, http.StatusInternalServerError,
        "INTERNAL_ERROR", "an unexpected error occurred", traceID)
}

// Penggunaan: handler mengikuti konvensi tanpa perlu tahu detail HTTP
func (h *UserHandler) GetUser(ctx context.Context, r *http.Request) (interface{}, error) {
    id := chi.URLParam(r, "id") // atau r.PathValue("id") di 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() akan handle terjemahan ke HTTP
    }
    return user, nil
}

// Router setup mengikuti konvensi
router.Get("/api/v1/users/{id}", httpkit.Adapt(userHandler.GetUser))
// Contoh lain: internal repository convention
// Setiap repository mengikuti interface yang sama

// pkg/repository/base.go
package repository

import "context"

// Konvensi: semua repository implement interface ini untuk operasi CRUD dasar
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
    // Field lain ditambahkan per-repository dengan embedding atau composition
}

// Repository yang mengikuti konvensi langsung dimengerti oleh engineer manapun
// yang sudah familiar dengan project ini
type UserRepository struct {
    db *sql.DB
}

// Karena mengikuti konvensi Base[User], engineer bisa langsung tahu
// method apa saja yang tersedia tanpa membuka file repository

CoC di Flutter/Dart #

Di Flutter, CoC paling terasa di struktur project dan penamaan file. Konvensi Dart yang ditegakkan oleh dart format dan dart analyze:

// Konvensi Dart yang sudah menjadi standar:

// 1. Nama file: snake_case
// user_service.dart, order_repository.dart, payment_gateway.dart

// 2. Nama class: PascalCase
class UserService {}
class OrderRepository {}
class PaymentGateway {}

// 3. Nama variabel dan method: camelCase
final userService = UserService();
Future<void> createOrder() async {}

// 4. Konstanta: camelCase (Dart) atau lowerCamelCase
const maxRetryCount = 3;
const defaultPageSize = 20;

// 5. Private: prefix underscore
class UserService {
    final UserRepository _repository; // private — konvensi Dart
    final Logger _logger;

    UserService(this._repository, this._logger);
}

// Struktur project Flutter yang mengikuti konvensi:
lib/
  ├── main.dart                      entry point  konvensi Flutter
  ├── app.dart                       MaterialApp / root widget
  ├── core/                          shared utilities, tidak tahu domain
     ├── network/
        └── dio_client.dart
     ├── error/
        └── exceptions.dart
     └── utils/
         └── validators.dart
  ├── features/                      fitur diorganisir per domain
     ├── auth/
        ├── data/
           ├── auth_repository_impl.dart
           └── auth_remote_source.dart
        ├── domain/
           ├── auth_repository.dart   interface
           └── user_entity.dart
        └── presentation/
            ├── auth_screen.dart
            └── auth_view_model.dart
     └── order/
         ├── data/
         ├── domain/
         └── presentation/
  └── shared/                        widget yang dipakai banyak fitur
      ├── widgets/
         ├── loading_indicator.dart
         └── error_view.dart
      └── theme/
          └── app_theme.dart

// Konvensi penamaan di Flutter:
// Screen (full page): UserProfileScreen, OrderListScreen
// Widget (komponen): UserAvatar, OrderCard, PriceTag
// ViewModel / Controller: UserProfileViewModel, OrderListController
// Repository interface: UserRepository, OrderRepository (tanpa Impl)
// Repository implementasi: UserRepositoryImpl, OrderRepositoryImpl

Kapan Konvensi Harus Dilanggar #

CoC bukan dogma. Ada situasi di mana menyimpang dari konvensi adalah keputusan yang tepat — selama penyimpangan itu disengaja, terdokumentasi, dan punya alasan yang jelas.

MENYIMPANG DARI KONVENSI YANG DIBENARKAN:

  1. Performance-critical code
     Konvensi ORM (GORM) menghasilkan query yang tidak optimal untuk
     laporan kompleks → tulis raw SQL yang dioptimasi
     → Dokumentasikan: "Raw SQL digunakan di sini karena ORM tidak bisa
       generate query plan yang efisien untuk aggregasi ini"

  2. Integrasi dengan sistem legacy
     Tabel database legacy punya nama tidak konsisten (tbl_usr, USER_DATA)
     → Tetap ikuti konvensi di kode Go, petakan ke nama legacy di repository
     → Jangan biarkan nama legacy bocor ke domain model

  3. Requirement teknis yang spesifik
     Endpoint dengan behavior non-standard karena integrasi dengan payment gateway
     yang punya requirement webhook khusus
     → Dokumentasikan mengapa endpoint ini berbeda dari konvensi REST biasa

  4. Domain yang memang punya konvensi berbeda
     Keuangan/akuntansi punya konvensi sendiri (debit/kredit, jurnal, ledger)
     → Ikuti konvensi domain, bukan konvensi teknis generik

MENYIMPANG YANG TIDAK DIBENARKAN:
  ✗ "Saya lebih suka nama ini" — preferensi personal bukan alasan cukup
  ✗ "Di project lama saya begini" — konsistensi dengan project ini lebih penting
  ✗ "Ini lebih fleksibel" — tanpa use case konkret yang membutuhkan fleksibilitas itu
  ✗ Tidak ada alasan yang bisa dijelaskan — konvensi harus diikuti

Aturan praktis: jika penyimpangan dari konvensi tidak bisa dijelaskan
dalam satu kalimat yang konkret, jangan lakukan.
CoC paling efektif ketika konvensinya terdokumentasi. Konvensi yang hanya ada di kepala engineer senior — tapi tidak pernah ditulis — adalah CoC yang akan hancur seiring pergantian tim. Dokumentasikan konvensi di ADR (Architecture Decision Record), README, atau CONTRIBUTING.md. Konvensi yang terdokumentasi bisa di-enforce di code review; yang tidak terdokumentasi hanya bisa di-enforce oleh orang yang ingat.

Mendokumentasikan dan Menegakkan Konvensi #

Konvensi yang tidak di-enforce adalah konvensi yang akan dilanggar. Ada beberapa cara untuk memastikan konvensi diikuti secara konsisten:

1. TOOLING OTOMATIS (paling efektif):
   gofmt / goimports      → format kode secara konsisten
   golangci-lint          → enforce naming convention, error handling patterns
   dart format            → format Dart secara konsisten
   dart analyze           → static analysis dengan custom lint rules
   Custom linter          → bisa dibuat untuk konvensi spesifik project

2. TEMPLATE DAN GENERATOR:
   go generate            → generate boilerplate yang mengikuti konvensi
   Template file          → starter template untuk handler, service, repository baru
   Makefile target        → "make new-handler NAME=user" → generate file dengan konvensi

3. CODE REVIEW CHECKLIST:
   Nama file mengikuti konvensi {domain}_{layer}.go?
   Response shape menggunakan helper writeSuccess/writeError?
   Error wrapped dengan fmt.Errorf("functionName: %w", err)?
   Test file ada di sebelah file yang ditest?

4. DOKUMENTASI:
   CONTRIBUTING.md        → konvensi yang perlu diikuti contributor
   ADR (Architecture Decision Record) → mengapa konvensi ini dipilih
   README per direktori   → penjelasan konvensi struktur direktori
# Makefile yang membantu menegakkan konvensi
.PHONY: new-handler new-service lint test

# Generate handler baru mengikuti konvensi
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 konvensi dengan linter
lint:
	golangci-lint run ./...
	go vet ./...

# Run semua test mengikuti konvensi Go testing
test:
	go test -race -cover ./...

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

Hubungan CoC dengan Prinsip Lain #

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

    DRY2["DRY\nKonvensi adalah DRY\ndi level keputusan —\nkeputusan yang sama tidak\nperlu dibuat berulang"]
    KISS2["KISS\nDefault yang cerdas\nmenghilangkan kompleksitas\nyang tidak perlu"]
    YAGNI2["YAGNI\nKonfigurasi hanya\nketika ada alasan nyata\nuntuk menyimpang"]
    SRP2["SRP\nSetiap layer punya\nkonvensi yang jelas\ntentang apa yang boleh\nada di dalamnya"]

    COC -->|"adalah DRY di level\nkeputusan desain"| DRY2
    COC -->|"menghasilkan"| KISS2
    COC -->|"sinergi dengan"| YAGNI2
    COC -->|"diperkuat oleh"| 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 adalah DRY di level keputusan desain: alih-alih menduplikasi keputusan yang sama di setiap tempat yang membutuhkannya, konvensi membuat keputusan itu sekali dan berlaku di mana-mana. YAGNI memperkuat CoC dari arah yang berlawanan: jangan buat konfigurasi untuk sesuatu yang konvensi defaultnya sudah cukup.


Anti-Pattern dalam Satu Pandangan #

// ✗ Konfigurasi eksplisit untuk sesuatu yang sudah punya konvensi
type UserRepository struct {
    db        *sql.DB
    tableName string // "users" — mengapa dikonfigurasi? konvensi sudah jelas
    idField   string // "id" — sudah jelas dari konvensi
}

// ✗ Penamaan yang tidak mengikuti konvensi Go tanpa alasan
type userservicehandler struct{}  // seharusnya UserServiceHandler atau dipecah
func (h *userservicehandler) doCreateUser() {} // seharusnya CreateUser

// ✗ Response shape yang tidak konsisten antar endpoint
// GET /users → {"users": [...], "count": 10}
// GET /orders → {"data": [...], "total": 10, "page": 1}
// GET /products → [...]  (array langsung, tanpa wrapper)

// ✗ Struktur folder yang tidak mencerminkan konvensi layer
internal/
  stuff/        // "stuff" bukan nama layer yang bermakna
  things/       // sama
  misc/         // ini package util dengan nama berbeda

// ✗ Error format yang tidak konsisten
return fmt.Errorf("error: %v", err)           // tidak ada konteks fungsi
return errors.New("something went wrong")      // tidak spesifik
return fmt.Errorf("UserService.Create: %v", err) // nama function, bukan "createUser"

// ✓ Konvensi error yang konsisten:
return fmt.Errorf("createUser %s: save to db: %w", email, err)

// ✗ Test yang tidak mengikuti konvensi Go
func testCreateUser(t *testing.T) {} // huruf kecil — tidak dijalankan go test!
// ✓ func TestCreateUser(t *testing.T) {}

Checklist Review CoC #

PENAMAAN:
  □ Nama package lowercase, satu kata, sama dengan nama direktori
  □ Interface satu method → nama = method + "er" (Reader, Writer, Stringer)
  □ Constructor → New + nama type (NewUserService, NewOrderHandler)
  □ Error variable → Err + nama kondisi (ErrNotFound, ErrUnauthorized)
  □ Test file → {filename}_test.go di direktori yang sama

STRUKTUR PROJECT:
  □ Direktori mencerminkan layer/concern yang jelas (domain, service, repository, api)
  □ Tidak ada direktori "util", "misc", "stuff", "things"
  □ Migration file → urutan numerik + deskripsi (001_create_users.sql)

API DAN RESPONSE:
  □ URL mengikuti konvensi REST (plural noun, nested untuk relasi)
  □ Response success menggunakan shape yang konsisten di semua endpoint
  □ Response error menggunakan shape yang konsisten (code, message, trace_id)
  □ Pagination parameter konsisten (page, per_page) di semua list endpoint

DATABASE:
  □ Nama tabel: snake_case, plural (users, order_items)
  □ Nama kolom: snake_case (user_id, created_at)
  □ Primary key selalu id
  □ Timestamp selalu created_at dan updated_at

KONVENSI YANG TERDOKUMENTASI:
  □ Konvensi project ada di CONTRIBUTING.md atau ADR
  □ Penyimpangan dari konvensi terdokumentasikan alasannya
  □ Linter atau tool otomatis digunakan untuk menegakkan konvensi

Ringkasan #

  • CoC berarti sistem punya default yang masuk akal — developer hanya perlu membuat keputusan eksplisit ketika ingin menyimpang. Bukan mengurangi fleksibilitas, tapi mengurangi keputusan yang tidak perlu.
  • Masalah yang diselesaikan: tanpa konvensi, setiap detail kecil menjadi diskusi yang menguras waktu. Dengan konvensi, engineer baru langsung tahu di mana mencari dan di mana meletakkan sesuatu — tanpa dokumentasi terpisah.
  • Go sudah punya CoC built-in: penamaan package, constructor NewX, interface Xer, error ErrX, file *_test.go, direktori internal/ — semua adalah konvensi yang dipahami seluruh ekosistem.
  • Project structure sebagai CoC: struktur direktori yang konsisten (domain/, service/, repository/, api/) membuat lokasi setiap komponen predictable dan mengurangi overhead navigasi.
  • API sebagai CoC: URL, HTTP method, response shape, dan pagination yang konsisten membuat endpoint predictable — client tidak perlu baca dokumentasi untuk menebak format yang benar.
  • Internal library sebagai enforcer: wrapper kecil untuk HTTP response, base repository interface, atau generator kode adalah cara tim menegakkan konvensi secara otomatis, bukan mengandalkan disiplin individu.
  • CoC perlu didokumentasikan: konvensi yang hanya ada di kepala engineer senior akan hancur seiring pergantian tim. Tulis di CONTRIBUTING.md, ADR, atau README per direktori.
  • Tooling adalah enforcer terbaik: gofmt, golangci-lint, dart format, dan custom linter menegakkan konvensi tanpa harus mengandalkan code review secara manual.
  • Menyimpang harus disengaja dan terdokumentasi: preferensi personal bukan alasan cukup. Penyimpangan yang valid punya alasan teknis konkret yang bisa dijelaskan dalam satu kalimat.
  • Hubungan dengan prinsip lain: CoC adalah DRY di level keputusan desain, sinergi dengan YAGNI (konfigurasi hanya saat ada alasan nyata), dan diperkuat oleh SRP (setiap layer punya konvensi yang jelas).

← Sebelumnya: Fail Fast   Berikutnya: Least Priviledge →

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