YAGNI — You Aren’t Gonna Need It #

Ada sebuah pola pikir yang sangat umum di kalangan engineer yang sudah membaca banyak buku tentang arsitektur: “Nanti pasti akan berkembang, lebih baik kita siapkan dari awal.” Hasilnya adalah sistem yang baru punya lima endpoint tapi sudah menggunakan microservices, Kafka, dan CQRS — karena “nanti pasti butuh”. Atau struct dengan dua puluh field filter padahal endpoint hanya butuh satu. Atau payment service dengan interface berlapis untuk provider yang belum ada kontraknya. Kode ini tidak gratis: ia butuh waktu untuk ditulis, butuh waktu untuk dipahami, butuh waktu untuk di-maintain, dan sering kali tidak pernah benar-benar dipakai. YAGNI — You Aren’t Gonna Need It — adalah prinsip yang menyerang pola pikir ini secara langsung: jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan. Bukan anti-perencanaan, bukan anti-arsitektur — tapi anti-asumsi berlebihan tentang masa depan yang belum terjadi.

Apa Itu YAGNI? #

YAGNI berasal dari praktik Extreme Programming (XP) yang dipopulerkan Ron Jeffries. Definisinya sederhana namun sering disalahpahami:

Jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan.

Kata kunci yang sering terlewat adalah “benar-benar” — bukan “mungkin nanti”, bukan “kata PM bulan depan”, bukan “biasanya sistem seperti ini butuh”. YAGNI berbicara tentang kebutuhan yang sudah ada requirement tertulisnya, sudah ada use case konkretnya, dan sudah ada validasi dari stakeholder bahwa ini memang prioritas.

YAGNI bukan larangan untuk berpikir ke depan. Ada perbedaan penting yang perlu dipahami:

YAGNI MELARANG:                         YAGNI TIDAK MELARANG:
─────────────────────────────────────   ────────────────────────────────────
Implementasi fitur yang belum           Interface yang kecil dan jelas
ada requirement-nya                     untuk dependency yang ada

Abstraksi multi-provider ketika         Struktur kode yang mudah diubah
baru ada satu provider konkret          tanpa perlu rewrite besar

Field atau parameter yang "mungkin      Error handling yang lengkap
nanti berguna"                          untuk path yang sudah ada

Layer arsitektur untuk scale yang       Logging dan observability
belum ada indikator akan dicapai        yang memang selalu dibutuhkan

Konfigurasi extensibility untuk         Security controls yang
fitur yang tidak ada di roadmap         merupakan requirement wajib

YAGNI dan extensible design bisa berjalan beriringan. Yang dilarang adalah implementasi fitur spekulatif — bukan desain yang mudah di-extend ketika waktunya tiba.

flowchart TD
    Q1{"Ada requirement\ntertulis?"}
    Q2{"Ada use case\nkonkret sekarang?"}
    Q3{"Biaya TIDAK\nmembuatnya sekarang\nlebih besar dari\nmembuatnya?"}
    BUILD["Buat sekarang ✓"]
    DEFER["Tunda — YAGNI ✓"]
    DESIGN["Desain agar\nmudah di-extend,\ntapi jangan\nimplementasi dulu"]

    Q1 -->|Ya| Q2
    Q1 -->|Tidak| DEFER
    Q2 -->|Ya| BUILD
    Q2 -->|Tidak| Q3
    Q3 -->|Ya| BUILD
    Q3 -->|Tidak| DESIGN

    style BUILD fill:#5CB85C,color:#fff
    style DEFER fill:#4C9BE8,color:#fff
    style DESIGN fill:#F0AD4E,color:#fff

Empat Masalah Nyata Kode yang Belum Dibutuhkan #

Kode spekulatif terasa seperti investasi tapi berperilaku seperti utang. Ini empat biaya nyata yang ditimbulkannya:

1. Biaya waktu penulisan yang tidak menghasilkan value sekarang. Setiap jam yang dihabiskan untuk fitur yang belum dibutuhkan adalah satu jam yang tidak dihabiskan untuk fitur yang sudah ada di backlog dan sudah ada penggunanya. Dalam sprint yang kompetitif, ini adalah trade-off yang nyata.

2. Biaya pemahaman yang terus berulang. Kode yang ada di codebase harus dipahami oleh setiap engineer yang membacanya — sekarang dan di masa depan. Abstraksi berlapis untuk sesuatu yang tidak digunakan memperlambat onboarding dan meningkatkan cognitive load saat debug.

3. Biaya maintenance yang tidak terduga. Setiap baris kode adalah surface area untuk bug. Kode yang ditulis untuk fitur spekulatif tetap perlu diupdate ketika ada refactor, ketika ada perubahan dependency, ketika ada kebutuhan migrasi. Kode yang tidak memberikan value tapi tetap membutuhkan perawatan adalah definisi utang teknis.

4. Biaya asumsi yang salah. Yang paling berbahaya: fitur yang dibangun berdasarkan asumsi tentang masa depan sering kali tidak sesuai dengan masa depan yang sebenarnya terjadi. Ketika requirement akhirnya datang, bentuknya berbeda dari yang diasumsikan, dan abstraksi yang sudah dibangun justru menjadi penghalang, bukan kemudahan.


YAGNI di Level Struct dan Field #

Bentuk pelanggaran YAGNI yang paling sering muncul dan paling mudah masuk ke codebase tanpa terdeteksi di code review adalah penambahan field atau parameter yang “mungkin nanti berguna”.

// ANTI-PATTERN: struct filter dengan semua kemungkinan field
// padahal endpoint hanya butuh filter berdasarkan name
type UserFilter struct {
    Name        *string    // ← dipakai sekarang
    Email       *string    // ← "nanti pasti butuh"
    Age         *int       // ← "mungkin untuk fitur segmentasi"
    Gender      *string    // ← "kalau ada personalisasi"
    City        *string    // ← "untuk regional targeting"
    Country     *string    // ← "kalau expand ke luar negeri"
    IsActive    *bool      // ← "untuk admin filter"
    IsVerified  *bool      // ← "kalau ada verifikasi"
    CreatedFrom *time.Time // ← "untuk laporan"
    CreatedTo   *time.Time // ← "untuk laporan"
    Tags        []string   // ← "kalau ada tagging system"
}

// Masalah:
// - Setiap field *string perlu nil check di query builder
// - Dokumentasi API menjadi membingungkan — field mana yang benar-benar bekerja?
// - Test harus cover berbagai kombinasi yang belum tentu pernah dipakai
// - Jika schema database berubah, semua field ini jadi noise

// BENAR: hanya field yang ada requirement-nya sekarang
type UserFilter struct {
    Name string // satu-satunya filter yang dibutuhkan saat ini
}

// Saat ada requirement baru untuk filter email:
// 1. Tambah field Email ke struct
// 2. Update query builder
// 3. Update dokumentasi
// Perubahan ini mudah dan terisolasi — tidak ada yang rusak
type UserFilter struct {
    Name  string
    Email string
}

Aturan yang sama berlaku untuk parameter fungsi:

// ANTI-PATTERN: parameter opsional untuk semua kemungkinan behavior
func CreateUser(
    name string,
    email string,
    role string,           // ← "nanti pasti butuh role"
    sendWelcomeEmail bool, // ← "mungkin ada yang tidak mau email"
    skipValidation bool,   // ← "untuk testing / admin"
    notifySlack bool,      // ← "kalau ada integrasi Slack"
    auditLog bool,         // ← "untuk compliance"
) error {
    // Setiap boolean parameter menggandakan jumlah path yang perlu ditest
    // 4 boolean = 16 kombinasi yang secara teoritis perlu diuji
}

// BENAR: hanya parameter yang ada kebutuhannya sekarang
func CreateUser(name, email string) error {
    // Sederhana, jelas, mudah ditest
    // Ketika butuh role: tambah parameter saat itu
    // Ketika butuh welcome email: tambah saat fitur email selesai didesain
}

YAGNI di Level Service dan Abstraksi #

Kasus paling klasik YAGNI adalah abstraksi multi-provider sebelum ada provider kedua.

// ANTI-PATTERN: arsitektur untuk masa depan yang belum ada
// Dibuat karena "nanti pasti akan ada banyak payment provider"

type PaymentProvider interface {
    Pay(ctx context.Context, amount int64, currency string) error
    Refund(ctx context.Context, txID string, amount int64) error
    Validate(ctx context.Context, txID string) (*PaymentStatus, error)
    GetWebhookSecret() string
    HandleWebhook(ctx context.Context, payload []byte) error
}

type PaymentProviderFactory interface {
    Create(providerType string, cfg ProviderConfig) (PaymentProvider, error)
}

type providerRegistry struct {
    mu        sync.RWMutex
    providers map[string]PaymentProvider
}

func (r *providerRegistry) Register(name string, p PaymentProvider) {
    r.mu.Lock()
    defer r.mu.Unlock()
    r.providers[name] = p
}

type PaymentService struct {
    registry *providerRegistry
    factory  PaymentProviderFactory
}

// Semua ini hanya untuk satu provider — Midtrans
// Fitur refund bahkan belum ada requirement-nya
// Interface Validate dan HandleWebhook belum pernah dipanggil dari manapun

// BENAR: sesuai kebutuhan nyata — satu provider, satu operasi yang ada
type PaymentService struct {
    midtrans *midtrans.Client
}

func NewPaymentService(client *midtrans.Client) *PaymentService {
    return &PaymentService{midtrans: client}
}

func (s *PaymentService) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
    resp, err := s.midtrans.CreateTransaction(ctx, &midtrans.TransactionRequest{
        OrderID:     req.OrderID,
        GrossAmount: req.Amount,
    })
    if err != nil {
        return nil, fmt.Errorf("createCharge %s: midtrans: %w", req.OrderID, err)
    }
    return &ChargeResult{TransactionID: resp.TransactionID, PaymentURL: resp.RedirectURL}, nil
}

Saat kebutuhan kedua provider benar-benar muncul, refactor-nya jelas dan terfokus:

// YAGNI yang sehat: refactor KETIKA ada requirement nyata
// Trigger: Product memutuskan mendukung Xendit untuk merchant tertentu

// Langkah 1: ekstrak interface berdasarkan operasi yang benar-benar ada
type PaymentGateway interface {
    CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error)
}

// Langkah 2: wrap implementasi yang sudah ada
type MidtransGateway struct{ client *midtrans.Client }
func (g *MidtransGateway) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
    // implementasi yang sudah ada, pindahkan ke sini
}

// Langkah 3: tambah implementasi baru
type XenditGateway struct{ client *xendit.Client }
func (g *XenditGateway) CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error) {
    // implementasi baru
}

// Langkah 4: update PaymentService untuk terima interface
type PaymentService struct {
    gateway PaymentGateway // sekarang baru ada nilai nyata dari abstraksi ini
}

Refactor ini jauh lebih mudah karena interface-nya dibentuk oleh kebutuhan nyata — bukan oleh asumsi tentang apa yang “mungkin dibutuhkan”. Interface yang muncul dari refactor seperti ini cenderung lebih tepat dan lebih kecil dari yang didesain di awal.

sequenceDiagram
    participant REQ as Requirement Datang
    participant NOW as Kode Sekarang
    participant REF as Refactor
    participant NEW as Implementasi Baru

    Note over REQ,NEW: Alur YAGNI yang Sehat

    REQ->>NOW: "Buat payment — Midtrans saja dulu"
    NOW->>NOW: PaymentService dengan *midtrans.Client langsung
    Note over NOW: Sederhana, berfungsi, mudah ditest

    REQ->>REF: "Butuh support Xendit untuk merchant enterprise"
    REF->>REF: Ekstrak PaymentGateway interface
    REF->>REF: Bungkus MidtransGateway
    REF->>NEW: Buat XenditGateway
    Note over REF,NEW: Refactor terfokus, interface tepat sasaran

YAGNI di Level API Design #

YAGNI di desain API berarti endpoint yang dibuat hanya untuk use case yang sudah ada, dengan response shape yang sesuai kebutuhan saat ini — bukan dengan semua field yang “mungkin berguna”.

// ANTI-PATTERN: response dengan semua field yang "mungkin dibutuhkan client"
type UserResponse struct {
    ID           string     `json:"id"`
    Name         string     `json:"name"`
    Email        string     `json:"email"`
    Role         string     `json:"role"`        // "nanti ada role-based UI"
    Permissions  []string   `json:"permissions"` // "kalau ada fine-grained access"
    LastLoginAt  *time.Time `json:"last_login_at"` // "untuk security audit"
    LoginCount   int        `json:"login_count"` // "untuk analytics"
    ProfileScore float64    `json:"profile_score"` // "untuk recommendation engine"
    Tags         []string   `json:"tags"`        // "kalau ada tagging"
    Metadata     map[string]interface{} `json:"metadata"` // "untuk custom field"
    // 10 field lagi yang belum pernah dibaca oleh client manapun
}

// Masalah:
// - Setiap field adalah contract yang harus dijaga kompatibilitasnya
// - Client yang di-generate otomatis menjadi bloated
// - Perubahan schema database berdampak luas karena banyak field yang di-expose
// - Tidak jelas field mana yang benar-benar dipakai client

// BENAR: hanya field yang dibutuhkan client yang ada sekarang
type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Saat ada kebutuhan nyata: tambah field, versi API, atau buat endpoint baru
// Menambah field ke response lebih mudah dari menghapusnya (breaking change)

YAGNI di API design juga berarti tidak membuat endpoint yang belum ada penggunanya:

// ANTI-PATTERN: endpoint untuk fitur yang belum ada
router.GET("/api/v1/users/:id/recommendations", handleRecommendations) // tidak ada ML model
router.GET("/api/v1/users/:id/social-score", handleSocialScore)         // belum ada konsep ini
router.POST("/api/v1/users/:id/import", handleBulkImport)               // tidak ada UI untuk ini
router.GET("/api/v1/analytics/cohort", handleCohortAnalysis)            // belum ada requirement

// BENAR: hanya endpoint yang ada penggunanya sekarang
router.POST("/api/v1/users", handleCreateUser)
router.GET("/api/v1/users/:id", handleGetUser)
router.PUT("/api/v1/users/:id", handleUpdateUser)

YAGNI di Level Database Schema #

Pelanggaran YAGNI yang paling mahal secara jangka panjang sering terjadi di schema database — karena migrasi database jauh lebih mahal dari refactor kode.

-- ANTI-PATTERN: tabel dengan kolom spekulatif
CREATE TABLE users (
    id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name        VARCHAR(255) NOT NULL,
    email       VARCHAR(255) UNIQUE NOT NULL,
    -- kolom yang ada requirement-nya sekarang selesai di sini --

    role        VARCHAR(50),          -- "nanti ada RBAC"
    tier        VARCHAR(20),          -- "nanti ada tier system"
    score       DECIMAL(5,2),         -- "untuk gamification"
    referral_code VARCHAR(20),        -- "nanti ada referral program"
    affiliate_id  UUID,               -- "nanti bisa jadi affiliate"
    last_login_at TIMESTAMP,          -- "untuk security dashboard"
    login_count   INTEGER DEFAULT 0,  -- "untuk analytics"
    extra_data    JSONB,              -- "catch-all untuk masa depan"
    -- 8 kolom yang tidak pernah diisi, NULL semua, ikut di setiap query
    created_at  TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at  TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Masalah:
-- Setiap INSERT harus menyertakan atau mengabaikan kolom-kolom ini
-- Index planning menjadi kompleks karena banyak kolom NULL
-- JOIN dan query terasa lebih berat karena row size besar
-- Schema evolution menjadi confusing: kolom mana yang sudah "aktif"?

-- BENAR: hanya kolom yang ada kebutuhannya sekarang
CREATE TABLE users (
    id         UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name       VARCHAR(255) NOT NULL,
    email      VARCHAR(255) UNIQUE NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);

-- Saat RBAC dibutuhkan: migrasi menambah kolom role
-- ALTER TABLE users ADD COLUMN role VARCHAR(50) NOT NULL DEFAULT 'user';
-- Migrasi ini mudah, terisolasi, dan bisa di-rollback

YAGNI di Level Arsitektur Sistem #

Pelanggaran YAGNI di level arsitektur adalah yang paling visible dan paling sering jadi bahan debat di awal project. Pertanyaan “monolith atau microservices?” hampir selalu dijawab salah ketika YAGNI diabaikan.

ANTI-PATTERN: Arsitektur untuk scale yang belum ada

Aplikasi baru, 5 endpoint, 3 engineer, belum ada user — tapi:
  ✗ Microservices karena "nanti pasti harus scale per service"
  ✗ Kafka karena "nanti pasti butuh async processing"
  ✗ CQRS + Event Sourcing karena "nanti butuh audit trail"
  ✗ Redis Cluster karena "nanti traffic pasti besar"
  ✗ Kubernetes dengan HPA karena "nanti perlu auto-scale"

Biaya nyata:
  - Waktu setup infrastruktur: 2–4 minggu
  - Kompleksitas debugging distributed system
  - Biaya cloud yang tidak sebanding dengan traffic
  - Network overhead antar service untuk setiap request
  - Engineer onboarding 3x lebih lama

BENAR: Mulai dengan arsitektur yang sesuai stage saat ini
  ✓ Monolith modular — mudah di-split jika benar-benar perlu
  ✓ REST API sederhana — tambah async ketika ada bottleneck nyata
  ✓ Database tunggal — pisahkan ketika ada kebutuhan isolation nyata
  ✓ Single instance — scale horizontal ketika ada traffic nyata
  ✓ Deployable ke satu VM — containerize ketika ada kebutuhan deployment nyata
flowchart LR
    subgraph YAGNI["Evolusi yang Mengikuti YAGNI"]
        direction TB
        S1["Stage 1\nMonolith + DB tunggal\n(startup, validasi produk)"]
        S2["Stage 2\nMonolith + Read replica\n(traffic mulai naik)"]
        S3["Stage 3\nModular monolith\n+ beberapa service terpisah\n(tim mulai besar)"]
        S4["Stage 4\nMicroservices untuk\nboundary yang jelas\n(scale berbeda per domain)"]
        S1 -->|"traffic naik,\nprofiling tunjukkan\nbottleneck nyata"| S2
        S2 -->|"tim > 15 orang,\ndeployment conflict\nmulai terjadi"| S3
        S3 -->|"scale requirement\nper domain berbeda\nsecara signifikan"| S4
    end

    style S1 fill:#5CB85C,color:#fff
    style S2 fill:#4C9BE8,color:#fff
    style S3 fill:#F0AD4E,color:#fff
    style S4 fill:#9B59B6,color:#fff

Setiap transisi dipicu oleh kebutuhan nyata yang terukur — bukan oleh antisipasi. Biaya migrasi dari monolith ke microservices memang ada, tapi jauh lebih kecil dari biaya mempertahankan arsitektur yang terlalu kompleks untuk kebutuhan saat ini.


Kapan YAGNI Tidak Boleh Diterapkan Ekstrem #

YAGNI bukan alasan untuk mengabaikan hal-hal yang secara inheren selalu dibutuhkan. Ada kategori kode yang tidak pernah spekulatif, tidak peduli seberapa awal stage-nya.

SELALU DIBUTUHKAN — BUKAN YAGNI VIOLATION:

  Security:
  ✓ Input validation — selalu dibutuhkan, bukan "nanti"
  ✓ SQL injection prevention — dari baris pertama kode
  ✓ Authentication untuk endpoint yang perlu auth
  ✓ Rate limiting untuk endpoint publik

  Reliability:
  ✓ Error handling yang proper — termasuk edge case
  ✓ Timeout untuk semua external call
  ✓ Context cancellation untuk long-running operation
  ✓ Graceful shutdown

  Observability:
  ✓ Logging untuk error dan operasi penting
  ✓ Health check endpoint
  ✓ Basic metrics untuk monitoring

  Data integrity:
  ✓ Database constraint (NOT NULL, UNIQUE, FK) sesuai schema
  ✓ Transaction untuk operasi multi-step
  ✓ Idempotency untuk operasi yang bisa di-retry
// INI BUKAN YAGNI — selalu perlu, bahkan di sprint pertama:

func CreateOrder(ctx context.Context, req CreateOrderRequest) (*Order, error) {
    // Input validation — BUKAN spekulatif
    if req.UserID == "" {
        return nil, errors.New("user_id is required")
    }
    if req.Amount <= 0 {
        return nil, errors.New("amount must be positive")
    }

    // Timeout untuk external call — BUKAN spekulatif
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    // Transaction untuk operasi multi-step — BUKAN spekulatif
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return nil, fmt.Errorf("createOrder: begin tx: %w", err)
    }
    defer tx.Rollback()

    order, err := orderRepo.SaveTx(ctx, tx, req)
    if err != nil {
        return nil, fmt.Errorf("createOrder: save order: %w", err)
    }

    if err := inventoryRepo.DeductTx(ctx, tx, req.Items); err != nil {
        return nil, fmt.Errorf("createOrder: deduct inventory: %w", err)
    }

    if err := tx.Commit(); err != nil {
        return nil, fmt.Errorf("createOrder: commit: %w", err)
    }

    // Logging untuk operasi penting — BUKAN spekulatif
    slog.Info("order created", "order_id", order.ID, "user_id", req.UserID)
    return order, nil
}

Cara membedakan “selalu dibutuhkan” dari “spekulatif”: apakah kode ini diperlukan agar sistem berfungsi dengan benar dan aman dalam use case yang ada sekarang? Jika ya — bukan YAGNI, wajib ada.


Refactor yang Sehat sebagai Pengganti Antisipasi #

YAGNI tidak berarti kode tidak pernah berubah. Justru sebaliknya — YAGNI mendorong refactor yang responsif terhadap kebutuhan nyata, bukan refactor preventif berdasarkan asumsi. Pola refactor yang sehat:

1. TULIS yang paling sederhana yang benar:
   → Implementasi langsung, konkret, tidak ada abstraksi prematur

2. VALIDASI bahwa ia bekerja:
   → Unit test, integration test, atau validasi ke pengguna

3. IDENTIFIKASI kebutuhan nyata yang muncul:
   → Requirement baru yang konkret, bukan asumsi
   → Duplikasi nyata yang perlu dihilangkan (DRY)
   → Bottleneck yang terukur, bukan yang dikira-kira

4. REFACTOR dengan tujuan yang jelas:
   → Ekstrak interface ketika ada implementasi kedua yang nyata
   → Tambah abstraksi ketika ada pengulangan knowledge
   → Pisahkan service ketika ada bottleneck scale yang terukur

5. ULANGI

Yang TIDAK dilakukan:
   → Refactor preventif "supaya nanti mudah diubah" tanpa kebutuhan konkret
   → Tambah abstraksi "supaya fleksibel" tanpa use case yang ada
   → Generalisasi untuk tipe yang belum ada

Hubungan YAGNI dengan Prinsip Lain #

flowchart TD
    YAGNI["YAGNI\n(You Aren't Gonna Need It)"]

    KISS["KISS\nJangan tambahkan\nkompleksitas yang\ntidak perlu"]
    DRY["DRY\nAbstrak hanya\nknowledge yang\nbenar-benar berulang"]
    SOLID["SOLID\nGunakan pattern\nhanya saat ada\nkebutuhan nyata"]
    AGILE["Agile / XP\nDeliver value\nsekarang, iterasi\nberdasarkan feedback"]

    YAGNI -->|"memperkuat"| KISS
    YAGNI -->|"mencegah abstraksi\nprematur di"| DRY
    YAGNI -->|"mencegah over-application\npada"| SOLID
    YAGNI -->|"berasal dari dan\nmendukung"| AGILE

    style YAGNI fill:#4C9BE8,color:#fff
    style KISS fill:#5CB85C,color:#fff
    style DRY fill:#5CB85C,color:#fff
    style SOLID fill:#5CB85C,color:#fff
    style AGILE fill:#F0AD4E,color:#fff

Hubungan YAGNI dengan DRY perlu perhatian khusus. DRY mendorong abstraksi untuk menghilangkan duplikasi — tapi abstraksi DRY yang dibuat terlalu awal bisa menjadi pelanggaran YAGNI. Solusinya adalah Rule of Three: biarkan kode terduplikasi sampai ada tiga occurrences, baru pertimbangkan abstraksi. Dua occurrences mungkin hanya kebetulan mirip.

YAGNI juga tidak bertentangan dengan SOLID. Terapkan SOLID ketika ada kebutuhan nyata yang diselesaikannya — interface untuk dependency yang memang akan ada implementasi lain, OCP untuk behavior yang memang akan di-extend. Jangan terapkan SOLID sebagai ritual.


Anti-Pattern dalam Satu Pandangan #

// ✗ Field yang "mungkin nanti berguna"
type CreateUserRequest struct {
    Name            string
    Email           string
    Role            string    // belum ada RBAC
    ReferralCode    string    // belum ada referral system
    AffiliateSource string    // belum ada affiliate tracking
}

// ✗ Interface multi-provider untuk satu provider
type NotificationProvider interface {
    SendEmail(to, subject, body string) error
    SendSMS(to, message string) error   // belum ada SMS provider
    SendPush(token, title, body string) error // belum ada push notif
    SendWhatsApp(to, message string) error   // belum ada WhatsApp
}
// Hanya ada email sekarang — tiga method lainnya belum pernah dipanggil

// ✗ Parameter boolean untuk semua kemungkinan behavior
func SendEmail(
    to, subject, body string,
    attachPDF bool,    // belum ada fitur PDF
    trackOpen bool,    // belum ada email tracking
    useTemplate bool,  // belum ada template engine
) error {}

// ✗ Arsitektur untuk scale yang belum ada
// Microservices + Kafka + Redis Cluster untuk aplikasi dengan 10 user aktif

// ✗ Konfigurasi extensibility tanpa use case
type PluginConfig struct {
    Enabled     bool
    MaxPlugins  int
    PluginPaths []string
    HookPoints  []string
}
// Tidak ada satu plugin pun yang ada atau direncanakan secara konkret

// ✗ Database kolom spekulatif
// score DECIMAL, referral_code VARCHAR, tier VARCHAR
// — semua NULL di semua row, tidak pernah dibaca

Checklist YAGNI Sebelum Commit #

SEBELUM MENAMBAHKAN FITUR ATAU ABSTRAKSI:
  □ Ada requirement tertulis yang jelas untuk ini?
  □ Ada use case konkret yang terjadi hari ini?
  □ Ada stakeholder yang meminta ini secara eksplisit?
  □ Jika tidak dibangun sekarang, apakah ada pengguna yang terdampak?

UNTUK INTERFACE DAN ABSTRAKSI:
  □ Ada lebih dari satu implementasi yang nyata (atau di roadmap konkret)?
  □ Ada use case testing yang membutuhkan mock dari interface ini?
  □ Interface ini menutup boundary yang benar-benar perlu diisolasi?

UNTUK DATABASE SCHEMA:
  □ Setiap kolom baru punya query yang membacanya?
  □ Tidak ada kolom yang hanya "untuk jaga-jaga"?
  □ Kolom nullable tidak digunakan sebagai workaround untuk schema yang belum jelas?

UNTUK ARSITEKTUR:
  □ Kompleksitas arsitektur sebanding dengan masalah yang ada sekarang?
  □ Ada metrik atau indikator nyata yang membenarkan kompleksitas ini?
  □ Bisa dijelaskan ke engineer baru dalam <10 menit?

PENGECUALIAN (tidak berlaku YAGNI):
  □ Security controls — selalu wajib ada
  □ Error handling dan timeout — bukan spekulatif
  □ Logging untuk operasi penting — bukan spekulatif
  □ Data integrity constraint — bukan spekulatif

Ringkasan #

  • YAGNI bukan anti-perencanaan — ia anti-asumsi berlebihan tentang masa depan yang belum terjadi. Perbedaan kuncinya: extensible design (desain yang mudah diubah) diizinkan; implementasi fitur spekulatif tidak.
  • Empat biaya kode yang belum dibutuhkan: waktu penulisan yang tidak menghasilkan value, biaya pemahaman yang berulang untuk setiap pembaca, biaya maintenance yang terus berjalan, dan risiko asumsi yang ternyata salah.
  • Di level struct dan field: tambahkan field atau parameter hanya saat ada requirement nyata. Setiap field *string atau *bool “untuk jaga-jaga” adalah kontrak yang harus dijaga dan surface area untuk bug.
  • Di level service dan abstraksi: mulai konkret dengan satu implementasi. Ekstrak interface hanya ketika ada kebutuhan nyata untuk implementasi kedua atau untuk mocking di test. Interface yang lahir dari refactor cenderung lebih tepat dari yang didesain di awal.
  • Di level API: response shape dan endpoint hanya untuk use case yang sudah ada. Menambah field ke response lebih mudah dari menghapusnya (breaking change).
  • Di level database schema: kolom spekulatif adalah utang paling mahal karena migrasi database mahal. Hanya buat kolom yang punya query yang membacanya.
  • Di level arsitektur: mulai dengan arsitektur yang sesuai stage saat ini — monolith modular sebelum microservices, database tunggal sebelum sharding. Naikkan kompleksitas dipicu oleh metrik nyata, bukan antisipasi.
  • YAGNI tidak berlaku untuk: security controls, error handling dan timeout, logging untuk operasi penting, dan data integrity constraint — ini bukan spekulatif, ini selalu dibutuhkan.
  • Refactor responsif lebih sehat dari antisipasi: tulis yang paling sederhana, validasi ia bekerja, refactor ketika ada kebutuhan nyata yang muncul. “Bangun untuk kebutuhan sekarang, desain agar mudah berubah besok.”

← Sebelumnya: KISS   Berikutnya: SRP →

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