YAGNI #

Ada pola yang sangat umum di codebase: fungsi dengan parameter reserved yang tidak pernah diisi, field database metadata_json yang kosong di setiap row, interface dengan method yang tidak pernah dipanggil, dan abstraksi berlapis untuk satu implementasi yang tidak pernah diganti. Semua itu adalah sisa dari keputusan “kita pasti akan butuh ini suatu hari nanti” — yang ternyata tidak pernah tiba. YAGNI — You Aren’t Gonna Need It — adalah prinsip dari Extreme Programming yang mengingatkan engineer bahwa kode yang belum dibutuhkan adalah utang, bukan investasi. Setiap baris kode yang ditulis harus dibaca, dipahami, ditest, dan dimaintain. Kode spekulatif memiliki semua biaya itu tanpa memberikan nilai apapun sampai (kalau) kebutuhannya benar-benar muncul. Panduan ini membahas YAGNI dari biaya nyata kode spekulatif, contoh konkret anti-pattern dan solusinya, YAGNI pada level arsitektur sistem, kapan refactor adalah pilihan yang lebih baik, dan batas-batas yang tidak boleh dilewati atas nama YAGNI.

Apa Itu YAGNI? #

YAGNI adalah singkatan dari You Aren’t Gonna Need It — jangan mengimplementasikan sesuatu sebelum benar-benar dibutuhkan. Prinsip ini berasal dari Extreme Programming (XP) dan Ron Jeffries, salah satu pendirinya.

“Always implement things when you actually need them, never when you just foresee that you need them.”

YAGNI bukan anti-perencanaan dan bukan berarti kode harus naif. Ada perbedaan penting antara dua hal:

Yang YAGNI larang:                    Yang YAGNI izinkan:
────────────────────────────────────  ────────────────────────────────────
Implementasi fitur yang belum         Desain yang mudah diubah
ada requirement-nya

Abstraksi untuk use case               Interface kecil yang jelas
yang belum ada

Method/parameter yang "mungkin         Struktur folder yang terorganisir
nanti berguna"

Pilihan arsitektur untuk scale         Kode yang mudah dites
yang belum terbukti dibutuhkan

Multi-provider untuk yang belum        Error handling yang proper
ada provider kedua

Perbedaan kuncinya: YAGNI melarang implementasi spekulatif, bukan desain yang bersih. Kamu boleh mendefinisikan interface bahkan jika hari ini hanya ada satu implementasi — asalkan ada kebutuhan nyata untuk testability atau substitusi yang sudah terdokumentasi. Yang tidak boleh adalah membangun sistem registry+factory+config hanya karena “nanti pasti akan ada lebih banyak”.


Biaya Nyata dari Kode yang Tidak Dipakai #

YAGNI lahir dari pengalaman nyata bahwa kode spekulatif memiliki biaya yang jarang diperhitungkan.

Biaya langsung: waktu yang dihabiskan untuk menulis, mereview, dan mentest kode yang belum dibutuhkan. Setiap jam yang dihabiskan untuk fitur spekulatif adalah jam yang tidak dihabiskan untuk fitur yang benar-benar dibutuhkan pengguna.

Biaya maintenance: setiap baris kode harus dibaca oleh engineer yang join belakangan. Kode yang tidak dipakai memperburuk signal-to-noise ratio codebase — engineer harus memahami apakah kode itu masih relevan atau dead code.

Biaya perubahan yang salah arah: jika requirement akhirnya datang, kemungkinan besar bentuknya berbeda dari yang diantisipasi. Kode spekulatif yang dibangun berdasarkan asumsi yang salah justru menjadi beban yang harus di-refactor.

Timeline kode spekulatif:

Sprint 1: Engineer membangun multi-provider payment system
          "Pasti nanti ada Xendit, DANA, GoPay..."
          Waktu: 3 hari extra

Sprint 2-10: Tidak ada requirement untuk provider kedua
             3 hari extra = 3 hari yang bisa untuk fitur lain

Sprint 11: Requirement datang: tambah Xendit
           Ternyata interface yang dibuat tidak sesuai
           (Xendit punya flow 3-step, bukan 1-step seperti yang diasumsikan)
           Waktu untuk refactor: 2 hari

Total overhead: 5 hari, lebih dari jika langsung build saat dibutuhkan

Contoh 1 — Payment Service #

Ini adalah contoh klasik YAGNI violation yang sering terjadi di awal project.

// ANTI-PATTERN: dibangun dengan asumsi "nanti pasti ada banyak provider"
// Padahal hari ini hanya ada Midtrans

type PaymentProvider interface {
    Pay(ctx context.Context, amount int64, currency string) (*PaymentResult, error)
    Refund(ctx context.Context, transactionID string, amount int64) error
    Validate(ctx context.Context, transactionID string) (*ValidationResult, error)
    GetStatus(ctx context.Context, transactionID string) (PaymentStatus, error)
}

type PaymentProviderRegistry struct {
    providers map[string]PaymentProvider
}

func (r *PaymentProviderRegistry) Register(name string, p PaymentProvider) {
    r.providers[name] = p
}

func (r *PaymentProviderRegistry) Get(name string) (PaymentProvider, error) {
    p, ok := r.providers[name]
    if !ok { return nil, fmt.Errorf("provider %s not found", name) }
    return p, nil
}

type MidtransProvider struct { apiKey string }
func (m *MidtransProvider) Pay(...) (*PaymentResult, error) { /* implementasi */ }
func (m *MidtransProvider) Refund(...) error { return nil } // fitur refund belum ada requirement
func (m *MidtransProvider) Validate(...) (*ValidationResult, error) { return nil, nil } // belum dipakai
func (m *MidtransProvider) GetStatus(...) (PaymentStatus, error) { return "", nil } // belum dipakai

// Padahal yang dibutuhkan hari ini hanya: proses pembayaran via Midtrans

// BENAR: implementasi minimal sesuai kebutuhan nyata
type PaymentService struct {
    midtransAPIKey string
    httpClient     *http.Client
}

func NewPaymentService(apiKey string) *PaymentService {
    return &PaymentService{
        midtransAPIKey: apiKey,
        httpClient:     &http.Client{Timeout: 30 * time.Second},
    }
}

func (s *PaymentService) CreateCharge(ctx context.Context, amount int64) (*ChargeResult, error) {
    // Implementasi Midtrans yang konkret dan langsung
    return s.callMidtrans(ctx, amount)
}

Kapan refactor ke interface adalah tepat? Ketika requirement provider kedua benar-benar datang, dan kamu sudah tahu bentuk interface yang benar berdasarkan dua implementasi nyata.

// Refactor yang tepat saat requirement datang:
// Sekarang ada Midtrans DAN Xendit dengan flow yang sudah dipahami

type PaymentGateway interface {
    // Interface didefinisikan berdasarkan kebutuhan NYATA dari dua provider
    CreateCharge(ctx context.Context, req ChargeRequest) (*ChargeResult, error)
}

// Interface ini lebih akurat karena dibuat setelah memahami keduanya

Contoh 2 — API Request Filter #

Menambahkan filter fields yang belum ada use case-nya adalah YAGNI violation yang sangat umum.

// ANTI-PATTERN: filter yang sangat lengkap tapi hanya name yang dipakai
type ListUsersFilter struct {
    Name        *string    // dipakai
    Email       *string    // belum ada requirement
    Age         *int       // belum ada requirement
    Gender      *string    // belum ada requirement
    City        *string    // belum ada requirement
    Country     *string    // belum ada requirement
    IsActive    *bool      // belum ada requirement
    CreatedFrom *time.Time // belum ada requirement
    CreatedTo   *time.Time // belum ada requirement
    Tags        []string   // belum ada requirement
}

// Setiap field tambahan perlu ditest, didokumentasikan, dan di-maintain

// BENAR: hanya apa yang dibutuhkan sekarang
type ListUsersFilter struct {
    Name string // satu-satunya filter yang ada di requirement saat ini
}

// Saat ada requirement baru untuk filter email:
// 1. Tambahkan field Email string
// 2. Update query
// 3. Update test
// Tidak perlu mengubah apapun yang sudah ada

Contoh 3 — Database Schema #

YAGNI juga berlaku pada database design. Kolom spekulatif bukan hanya pemborosan storage — ia menciptakan confusion tentang semantics dan sulit dihapus setelah production.

-- ANTI-PATTERN: kolom spekulatif yang "mungkin nanti berguna"
CREATE TABLE orders (
    id            UUID PRIMARY KEY,
    user_id       UUID NOT NULL,
    total         BIGINT NOT NULL,
    status        VARCHAR(50) NOT NULL,
    created_at    TIMESTAMP NOT NULL,

    -- Kolom-kolom berikut belum ada requirement
    metadata_json TEXT,           -- "nanti mungkin butuh extra data"
    source_system VARCHAR(100),   -- "mungkin nanti multi-channel"
    external_ref  VARCHAR(255),   -- "mungkin nanti perlu external ID"
    deleted_at    TIMESTAMP,      -- soft delete belum diminta
    updated_by    UUID,           -- audit trail belum diminta
    version       INTEGER         -- optimistic locking belum diminta
);

-- BENAR: hanya kolom yang ada requirement nyata
CREATE TABLE orders (
    id         UUID PRIMARY KEY,
    user_id    UUID NOT NULL,
    total      BIGINT NOT NULL,
    status     VARCHAR(50) NOT NULL,
    created_at TIMESTAMP NOT NULL
);

-- Saat soft delete diperlukan: ALTER TABLE orders ADD COLUMN deleted_at TIMESTAMP
-- Saat audit trail diperlukan: buat tabel audit terpisah
-- Mudah ditambah, tidak bisa dihapus tanpa migration yang kompleks
Kolom database jauh lebih sulit dihapus daripada ditambahkan — sekali ada data production di kolom tersebut, menghapusnya memerlukan data migration yang berisiko. Ini salah satu alasan YAGNI pada database schema sangat penting: tambahkan kolom saat ada kebutuhan nyata, bukan antisipasi.

Contoh 4 — YAGNI dalam Dart/Flutter #

Di Flutter, YAGNI sering dilanggar pada level state management dan widget API.

// ANTI-PATTERN: widget dengan terlalu banyak konfigurasi yang belum dipakai
class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback? onTap;
  final VoidCallback? onLongPress;       // belum ada requirement
  final VoidCallback? onDoubleTap;       // belum ada requirement
  final Color? backgroundColor;          // belum ada requirement
  final BorderRadius? borderRadius;      // belum ada requirement
  final EdgeInsets? padding;             // belum ada requirement
  final bool showBadge;                  // belum ada requirement
  final String? badgeText;               // belum ada requirement
  final bool isAnimated;                 // belum ada requirement

  const ProductCard({
    required this.product,
    this.onTap,
    this.onLongPress,
    this.onDoubleTap,
    this.backgroundColor,
    this.borderRadius,
    this.padding,
    this.showBadge = false,
    this.badgeText,
    this.isAnimated = false,
  });
  // ...
}

// BENAR: hanya parameter yang benar-benar diperlukan saat ini
class ProductCard extends StatelessWidget {
  final Product product;
  final VoidCallback onTap; // satu-satunya interaksi yang ada di requirement

  const ProductCard({
    required this.product,
    required this.onTap,
  });

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Card(
        child: Column(children: [
          Image.network(product.imageUrl),
          Text(product.name),
          Text('Rp ${product.price}'),
        ]),
      ),
    );
  }
}

// Saat ada requirement untuk long press: tambahkan onLongPress?
// Saat ada requirement untuk badge: tambahkan showBadge dan badgeText

YAGNI dalam Arsitektur Sistem #

YAGNI paling sering dilanggar di level arsitektur — keputusan yang paling mahal untuk diubah.

Skenario startup dengan 10 user sehari:

ANTI-YAGNI:
  Kubernetes cluster multi-region
  Kafka untuk async processing
  CQRS + Event Sourcing
  Service mesh (Istio)
  Dedicated auth service
  Feature flag system
  A/B testing infrastructure

Biaya: 3 bulan setup, tim kecil exhaust, fitur bisnis tertunda

YAGNI:
  Monolith sederhana atau modular monolith
  PostgreSQL single instance
  Redis untuk cache
  Background job dengan queue sederhana (database-backed)

Biaya: 2 minggu, tim fokus pada fitur bisnis
Kapan evolusi arsitektur: saat ada masalah nyata yang membutuhkannya

Panduan evolusi arsitektur berdasarkan YAGNI:

Traffic < 1000 req/day:
  Monolith, single DB → cukup
  Optimasi query sebelum scaling

Traffic 1k-100k req/day:
  Read replica
  Caching strategis
  Background jobs untuk operasi berat

Traffic > 100k req/day:
  Mulai pertimbangkan microservices untuk komponen yang butuh scale berbeda
  Message queue untuk decoupling
  Horizontal scaling

Bukan: "kita pasti akan dapat 1 juta user, siapkan dari awal"

Refactor adalah Solusi yang Lebih Baik dari Antisipasi #

Salah satu alasan engineer melanggar YAGNI adalah ketakutan: “jika kita tidak bangun sekarang, akan sangat mahal untuk mengubahnya nanti.” Tapi asumsi ini sering salah.

// Contoh refactoring yang sehat — tidak menakutkan jika kode terstruktur baik

// Versi 1 (YAGNI): hanya Midtrans
type PaymentService struct {
    apiKey string
}

func (s *PaymentService) CreateCharge(ctx context.Context, amount int64) (*Charge, error) {
    return s.callMidtrans(ctx, amount)
}

// Versi 2 (saat Xendit dibutuhkan): refactor yang terkontrol
type PaymentGateway interface {
    CreateCharge(ctx context.Context, amount int64) (*Charge, error)
}

type PaymentService struct {
    gateway PaymentGateway // sekarang bisa di-inject
}

func NewPaymentService(gateway PaymentGateway) *PaymentService {
    return &PaymentService{gateway: gateway}
}

// Semua caller yang sudah ada tidak perlu berubah —
// hanya tempat di-construct yang berubah (dari:)
svc := &PaymentService{apiKey: key}
// (menjadi:)
svc := NewPaymentService(&MidtransGateway{apiKey: key})
// atau
svc := NewPaymentService(&XenditGateway{apiKey: key})

Refactor mudah dilakukan ketika kode memenuhi prinsip dasar yang lain (SRP, DI) — itu yang membuat YAGNI aman diterapkan. Kode yang terstruktur dengan baik bisa dievolusi tanpa rewrite besar.


Batas YAGNI yang Tidak Boleh Dilangkahi #

YAGNI bukan izin untuk mengabaikan hal-hal fundamental. Ada kategori hal yang selalu perlu meski belum ada requirement eksplisit.

SELALU perlu (bukan YAGNI violation):
  □ Error handling yang proper — kode tanpa error handling adalah bug menunggu terjadi
  □ Input validation — security dan data integrity bukan fitur spekulatif
  □ Logging yang cukup untuk debugging — tanpa ini, production issues sulit dilacak
  □ Timeout untuk network calls — blocking indefinitely bukan fitur, itu bug
  □ Retry untuk transient errors — infrastruktur tidak selalu reliable
  □ Unit test untuk business logic — test bukan fitur spekulatif
  □ Basic security: SQL injection prevention, XSS prevention, dll
  □ Graceful shutdown — proses yang tidak bisa dihentikan dengan bersih adalah masalah

BOLEH ditunda (YAGNI berlaku):
  □ Admin dashboard — sampai ada operator yang butuhnya
  □ Export CSV/Excel — sampai ada user yang minta
  □ Multi-language support — sampai ada user non-Indonesia
  □ Advanced filtering — sampai ada use case yang konkret
  □ Audit trail lengkap — sampai ada requirement compliance
  □ Microservices — sampai ada alasan scale yang jelas

Checklist YAGNI Sebelum Menulis Kode #

Sebelum menambahkan fitur/abstraksi, tanyakan:

  □ Ada requirement tertulis atau ticket yang mendeskripsikan ini?
  □ Ada user atau stakeholder yang konkret meminta ini sekarang?
  □ Jika tidak diimplementasikan hari ini, apakah ada konsekuensi nyata?
  □ Apakah ini bisa ditambahkan nanti tanpa harus rewrite besar?

Jika semua jawaban "tidak/tidak ada" → tunda.

Pengecualian:
  □ Ini menyangkut security atau data integrity?
  □ Ini menyangkut error handling atau stability?
  □ Biaya untuk menambahkan nanti jauh lebih tinggi dari sekarang?

Jika ada satu jawaban "ya" → boleh lanjutkan.

Ringkasan #

  • YAGNI berarti jangan implementasikan sebelum benar-benar dibutuhkan — kode spekulatif adalah utang dengan bunga: waktu tulis, waktu test, waktu maintain, dan kemungkinan harus diubah saat requirement nyata datang berbeda dari asumsi.
  • Bukan anti-desain: YAGNI melarang implementasi spekulatif, bukan desain yang bersih. Interface untuk testability boleh, registry+factory untuk provider yang belum ada tidak.
  • Payment service: mulai dengan implementasi konkret satu provider, refactor ke interface saat ada kebutuhan multi-provider yang nyata.
  • Database schema: jangan tambahkan kolom spekulatif — lebih mudah menambah kolom saat dibutuhkan daripada menghapus kolom yang sudah ada data.
  • API filter: tambahkan field filter hanya saat ada use case yang jelas, bukan antisipasi semua kemungkinan.
  • Arsitektur: mulai dengan monolith yang terstruktur baik, evolusi ke microservices saat ada masalah scale yang nyata dan terukur.
  • Refactor adalah normal: kode yang terstruktur dengan baik bisa dievolusi kapanpun dibutuhkan — ini yang membuat YAGNI aman.
  • Batas yang tidak boleh dilangkahi: error handling, input validation, security dasar, timeout, logging, dan unit test bukan fitur spekulatif — selalu implementasikan.

← Sebelumnya: KISS   Berikutnya: SRP →

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