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 adalahusers. 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:#fffCoC 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:#fffCoC 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, interfaceXer, errorErrX, file*_test.go, direktoriinternal/— 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).