KISS — Keep It Simple #
Ada paradoks yang hampir selalu muncul seiring bertambahnya pengalaman seorang engineer: semakin dalam pemahamannya tentang pattern, abstraksi, dan arsitektur, semakin besar godaan untuk menerapkannya di mana-mana. Kode yang memiliki banyak layer terasa lebih “enterprise”. Interface untuk setiap dependency terasa lebih “testable”. Generic function yang bisa handle semua tipe sekaligus terasa lebih “reusable”. Padahal, di balik semua kompleksitas yang tidak perlu itu, tersembunyi biaya nyata: waktu onboarding yang lebih lama, bug yang lebih sulit dilacak, refactor yang lebih mahal, dan sistem yang semakin sulit dipahami setiap kali ada engineer baru bergabung. KISS — Keep It Simple, Stupid — adalah pengingat bahwa kesederhanaan yang disengaja adalah keahlian tertinggi dalam software engineering, bukan kompromi. Panduan ini membahas apa yang benar-benar dimaksud KISS, bagaimana membedakannya dari kode yang naif atau setengah jadi, tanda-tanda over-engineering yang perlu diwaspadai, contoh konkret dari level fungsi hingga arsitektur di Go dan Dart, serta kapan kompleksitas memang diperlukan dan tidak boleh disederhanakan paksa.
Apa yang Dimaksud KISS? #
KISS adalah singkatan dari Keep It Simple, Stupid — kadang diinterpretasikan sebagai Keep It Simple and Straightforward. Prinsipnya satu: pilih solusi paling sederhana yang benar-benar menyelesaikan masalah, tanpa menambahkan kompleksitas yang tidak memberi nilai nyata.
Tapi “sederhana” di sini bukan berarti ceroboh, tidak ada error handling, atau mengabaikan edge case. Ada perbedaan yang penting antara sederhana yang baik, naif, dan kompleks yang tidak perlu:
SEDERHANA (KISS ✓): NAIF (bukan KISS ✗):
──────────────────────────────── ────────────────────────────────
Alur logika mudah diikuti Tidak ada error handling
Struktur jelas dan eksplisit Abaikan edge case penting
Hanya kode yang diperlukan Kode yang belum selesai
Mudah dipahami engineer lain Hanya mudah bagi penulisnya
KOMPLEKS TIDAK PERLU (✗): KOMPLEKS YANG DIBENARKAN (✓):
──────────────────────────────── ────────────────────────────────
Interface untuk satu implementasi Interface karena ada beberapa
yang tidak akan pernah diganti implementasi berbeda yang nyata
Generic untuk tipe yang sudah Generic karena memang ada
diketahui dan tidak akan berubah multiple types yang valid
Layer abstraksi yang hanya Layer abstraksi yang menambahkan
meneruskan panggilan tanpa behavior: validasi, transform,
menambah nilai apapun logging, caching
Factory + Builder untuk struct Factory ketika pembuatan objek
yang bisa di-instantiate langsung memiliki logika kondisional nyata
Pertanyaan yang selalu harus ditanyakan sebelum menambahkan kompleksitas: “Apakah ini menyelesaikan masalah nyata yang ada sekarang?” Jika jawabannya tidak, atau “mungkin suatu hari nanti” — itu tanda untuk tetap sederhana.
flowchart TD
Q1{"Ada kebutuhan\nnyata sekarang?"}
Q2{"Solusi sederhana\ncukup menyelesaikannya?"}
Q3{"Kompleksitas menambah\nnilai yang jelas?"}
SIMPLE["Gunakan solusi\npaling sederhana ✓"]
COMPLEX["Kompleksitas\ndibenarkan ✓"]
OVER["Over-engineering ✗\nTolak kompleksitas ini"]
Q1 -->|Ya| Q2
Q1 -->|Tidak| OVER
Q2 -->|Ya| SIMPLE
Q2 -->|Tidak| Q3
Q3 -->|Ya| COMPLEX
Q3 -->|Tidak| OVER
style SIMPLE fill:#5CB85C,color:#fff
style COMPLEX fill:#4C9BE8,color:#fff
style OVER fill:#D9534F,color:#fffTujuh Tanda Over-Engineering #
Over-engineering adalah pelanggaran KISS yang paling umum dan paling berbahaya karena tidak terlihat seperti masalah di awal — bahkan terlihat seperti kualitas tinggi. Ini adalah tanda-tandanya:
1. FRAMEWORK INTERNAL LEBIH BESAR DARI BUSINESS LOGIC
Lebih banyak kode untuk mendukung "infrastruktur" internal (pipeline,
registry, plugin system) daripada untuk logika bisnis yang sebenarnya.
Sinyal: "sebelum bisa tambah fitur baru, harus register di 3 tempat dulu."
2. SULIT DIJELASKAN KE ENGINEER BARU
Butuh waktu >5 menit untuk menjelaskan alur yang sebenarnya sederhana.
Sinyal: "ini memang sedikit kompleks, nanti aku jelaskan step by step."
3. INTERFACE TANPA IMPLEMENTASI KEDUA
Ada interface yang hanya punya satu implementasi konkret dan tidak ada
rencana konkret (bukan "mungkin nanti") untuk implementasi lain.
Sinyal: interface dibuat "untuk extensibility" tapi tidak ada yang tahu
extensibility seperti apa yang dimaksud.
4. PASS-THROUGH LAYER
Ada layer abstraksi yang hanya meneruskan panggilan ke layer di bawahnya
tanpa menambahkan behavior, validasi, atau transformasi apapun.
Sinyal: "kenapa ada UseCase yang hanya memanggil Repository tanpa logic?"
5. GENERIC UNTUK SATU TIPE
Ada generic/template yang hanya pernah digunakan dengan satu tipe konkret
dan tidak ada rencana penggunaan dengan tipe lain.
Sinyal: `Result[T]` yang selalu dipakai sebagai `Result[UserData]` saja.
6. EXTENSIBILITY TANPA ROADMAP
Ada konfigurasi atau plugin system untuk fitur yang belum pernah diminta,
tidak ada di roadmap, dan tidak ada stakeholder yang memintanya.
Sinyal: "kita buat generic dulu, nanti bisa extend kalau butuh."
7. FLEKSIBILITAS MASA DEPAN YANG TIDAK KONKRET
Kode yang "fleksibel untuk masa depan" tapi tidak ada yang bisa menjelaskan
masa depan yang mana, kapan, dan siapa yang memintanya.
Sinyal: "ini kita buat abstrak supaya nanti mudah diubah."
Kompleksitas memiliki biaya nyata yang tidak selalu terlihat di sprint pertama: waktu onboarding yang lebih lama, bug yang lebih sulit dilacak karena banyak indirection, refactoring yang lebih mahal karena abstraksi sudah tersebar, dan kode yang semakin susah dipahami setiap kali ada engineer baru bergabung.
Fungsi yang Jelas vs Fungsi yang “Pintar” #
Pelanggaran KISS yang paling sering muncul di code review adalah kode yang clever — kode yang terasa elegan bagi penulisnya tapi membingungkan bagi pembaca. Cleverness dan clarity adalah dua hal yang berbeda.
// ANTI-PATTERN: terlalu banyak nested condition, tipe tidak jelas
// Engineer baru harus membaca semua baris untuk memahami kontrak fungsi ini
func IsAdult(user map[string]interface{}) bool {
if age, ok := user["age"]; ok {
if ageInt, ok := age.(int); ok {
if ageInt >= 18 {
return true
} else {
return false
}
}
}
return false
}
// BENAR: tipe eksplisit, logika satu baris, kontrak langsung jelas dari signature
type User struct {
Age int
}
func (u User) IsAdult() bool {
return u.Age >= 18
}
Versi KISS lebih pendek, lebih aman (tipe diperiksa saat compile, bukan runtime), lebih mudah ditest, dan kontraknya langsung jelas dari signature tanpa membaca implementasi. Penggunaan map[string]interface{} untuk struct yang sudah diketahui bentuknya adalah over-generalization yang tidak memberi manfaat apapun.
// ANTI-PATTERN: chaining panjang yang membuat debug hampir mustahil
// Jika ada error, dari layer mana? Tidak bisa tahu tanpa debugger
func ReadConfig() (*Config, error) {
return parse(validate(load(readFile("config.yaml"))))
}
// BENAR: setiap langkah eksplisit dengan konteks error yang jelas
func ReadConfig(path string) (*Config, error) {
raw, err := readFile(path)
if err != nil {
return nil, fmt.Errorf("readConfig: read file: %w", err)
}
data, err := load(raw)
if err != nil {
return nil, fmt.Errorf("readConfig: load: %w", err)
}
cfg, err := parse(data)
if err != nil {
return nil, fmt.Errorf("readConfig: parse: %w", err)
}
if err := validate(cfg); err != nil {
return nil, fmt.Errorf("readConfig: validate: %w", err)
}
return cfg, nil
}
Versi kedua lebih panjang, tapi ketika ada error di production, pesan "readConfig: parse: unexpected token at line 12" langsung menunjuk ke masalahnya. Chaining parse(validate(load(...))) hanya menghasilkan error yang tidak bisa dilacak tanpa breakpoint.
Guard Clause — Teknik KISS Paling Efektif #
Nested condition adalah salah satu bentuk kompleksitas tersembunyi yang paling sering ditemukan dan paling mudah diperbaiki. Guard clause (early return) adalah solusinya: validasi semua precondition di awal, kembalikan error segera, dan biarkan logic utama berjalan di level teratas tanpa nesting.
// ANTI-PATTERN: deeply nested — mental model sangat sulit dibangun
// Engineer harus membayangkan semua cabang secara bersamaan
func ProcessOrder(order *Order) error {
if order != nil {
if order.UserID != "" {
if order.Total > 0 {
if order.Status == "pending" {
if err := validateItems(order.Items); err == nil {
// logic utama terkubur di level 5
if err := saveOrder(order); err == nil {
return notifyUser(order.UserID)
} else {
return fmt.Errorf("save: %w", err)
}
} else {
return fmt.Errorf("items: %w", err)
}
} else {
return errors.New("order not pending")
}
} else {
return errors.New("invalid total")
}
} else {
return errors.New("missing user id")
}
}
return errors.New("order is nil")
}
// BENAR: guard clause — setiap baris bisa dibaca independen
// Logic utama ada di bawah, tidak terkubur nesting
func ProcessOrder(order *Order) error {
if order == nil {
return errors.New("order is nil")
}
if order.UserID == "" {
return errors.New("missing user id")
}
if order.Total <= 0 {
return errors.New("invalid total")
}
if order.Status != "pending" {
return errors.New("order not pending")
}
if err := validateItems(order.Items); err != nil {
return fmt.Errorf("validate items: %w", err)
}
// Logic utama ada di level teratas — jelas dan mudah ditemukan
if err := saveOrder(order); err != nil {
return fmt.Errorf("save order: %w", err)
}
return notifyUser(order.UserID)
}
flowchart LR
subgraph ANTI["Anti-Pattern: Nested"]
direction TB
A1["if order != nil"]
A2[" if userID != ''"]
A3[" if total > 0"]
A4[" if status == pending"]
A5[" → logic utama (level 4)"]
A1 --> A2 --> A3 --> A4 --> A5
end
subgraph GOOD["KISS: Guard Clause"]
direction TB
G1["if order == nil → return error"]
G2["if userID == '' → return error"]
G3["if total <= 0 → return error"]
G4["if status != pending → return error"]
G5["→ logic utama (level 0)"]
G1 --> G2 --> G3 --> G4 --> G5
end
style A5 fill:#D9534F,color:#fff
style G5 fill:#5CB85C,color:#fffRule of thumb: jika sebuah fungsi memiliki nesting lebih dari dua level, hampir selalu bisa disederhanakan dengan guard clause. Setiap level nesting tambahan meningkatkan cognitive load secara eksponensial — pembaca harus mempertahankan semua kondisi di kepala mereka secara bersamaan.
Abstraksi Prematur #
Abstraksi yang dibangun sebelum ada kebutuhan nyata adalah pelanggaran KISS yang paling sering dilakukan oleh engineer yang sudah membaca banyak buku tentang design pattern.
// ANTI-PATTERN: factory + interface untuk satu implementasi
// yang tidak akan pernah diganti
type PaymentProcessorFactory interface {
Create(config PaymentConfig) PaymentProcessor
}
type PaymentProcessor interface {
Process(ctx context.Context, req PaymentRequest) (*PaymentResult, error)
Validate(req PaymentRequest) error
Rollback(txID string) error
GetFee(amount int64) float64
}
type DefaultPaymentProcessorFactory struct{}
func (f *DefaultPaymentProcessorFactory) Create(cfg PaymentConfig) PaymentProcessor {
return &StripeProcessor{apiKey: cfg.APIKey}
}
// Service harus melewati factory hanya untuk dapat processor
// yang selalu Stripe — tidak pernah yang lain
type PaymentService struct {
factory PaymentProcessorFactory // selalu DefaultPaymentProcessorFactory
}
func NewPaymentService(factory PaymentProcessorFactory) *PaymentService {
return &PaymentService{factory: factory}
}
// Caller:
factory := &DefaultPaymentProcessorFactory{}
service := NewPaymentService(factory)
// 3 layer untuk sesuatu yang bisa jadi 1 baris
// BENAR: langsung dan eksplisit sesuai kebutuhan saat ini
type PaymentService struct {
stripeClient *stripe.Client
}
func NewPaymentService(stripeClient *stripe.Client) *PaymentService {
return &PaymentService{stripeClient: stripeClient}
}
func (s *PaymentService) CreateCharge(ctx context.Context, amount int64, currency string) error {
params := &stripe.ChargeParams{
Amount: stripe.Int64(amount),
Currency: stripe.String(currency),
}
_, err := s.stripeClient.Charges.New(params)
return err
}
// Nanti, KETIKA ada kebutuhan nyata untuk multiple payment provider:
type PaymentGateway interface {
Charge(ctx context.Context, amount int64, currency string) error
Refund(ctx context.Context, chargeID string) error
}
// Baru saat itu abstraksi ini punya nilai — dan implementasi konkret
// yang ada bisa di-wrap untuk memenuhi interface ini
Pola yang benar adalah: mulai konkret, ekstrak abstraksi ketika ada kebutuhan nyata yang muncul. Bukan sebaliknya.
Desain API yang Eksplisit vs Generic #
KISS sangat relevan di desain API. Endpoint yang terlalu generic terlihat “fleksibel” tapi sebenarnya lebih sulit digunakan, lebih sulit didokumentasikan, dan lebih sulit di-secure.
// ANTI-PATTERN: satu endpoint generic untuk semua operasi
POST /api/v1/action
{
"type": "create_user",
"payload": { "name": "Budi", "email": "[email protected]" }
}
POST /api/v1/action
{
"type": "update_user",
"id": "usr-123",
"payload": { "name": "Budi Santoso" }
}
POST /api/v1/action
{
"type": "delete_user",
"id": "usr-123"
}
Masalah yang ditimbulkan:
✗ Tidak bisa pakai HTTP method yang semantically benar (PUT, DELETE)
✗ Swagger/OpenAPI tidak bisa generate dokumentasi yang akurat
✗ Authorization per resource type sulit — harus parse body dulu
✗ Rate limiting per operation type jadi kompleks
✗ Caching tidak bisa memanfaatkan HTTP semantics (GET idempoten)
✗ Semua client harus tahu "type" string yang valid — tidak type-safe
// BENAR: endpoint eksplisit sesuai resource dan HTTP semantics
POST /api/v1/users → buat user baru (201 Created)
GET /api/v1/users/:id → ambil user (200 OK, cacheable)
PUT /api/v1/users/:id → update user lengkap (200 OK, idempoten)
PATCH /api/v1/users/:id → update user sebagian (200 OK)
DELETE /api/v1/users/:id → hapus user (204 No Content, idempoten)
Keuntungan:
✓ Dokumentasi OpenAPI otomatis akurat per endpoint
✓ Authorization middleware bisa check per route + method
✓ Rate limiting bisa per route + method
✓ GET request bisa di-cache di CDN dan browser
✓ Client SDK yang di-generate otomatis type-safe
✓ Engineer baru langsung paham tanpa membaca dokumentasi panjang
flowchart LR
subgraph ANTI["Anti-Pattern: Generic Endpoint"]
C1["POST /api/action\n{type: 'create_user'}"]
C2["POST /api/action\n{type: 'update_user'}"]
C3["POST /api/action\n{type: 'delete_user'}"]
end
subgraph GOOD["KISS: Explicit REST"]
R1["POST /users"]
R2["PUT /users/:id"]
R3["DELETE /users/:id"]
end
style ANTI fill:#fff3cd
style GOOD fill:#d4eddaError Handling yang KISS #
Go secara filosofis menerapkan KISS dalam error handling — error adalah nilai biasa yang dikembalikan, bukan exception tersembunyi yang bisa muncul dari mana saja. Tapi pola error handling yang salah tetap bisa membuat debugging menjadi mimpi buruk.
// ANTI-PATTERN: wrap error berlebihan, redundan, tidak informatif
func getUserData(id string) (*UserData, error) {
user, err := repo.FindUser(id)
if err != nil {
// "error: error" tidak berguna sama sekali
return nil, fmt.Errorf("error: %v", err)
}
profile, err := repo.FindProfile(user.ProfileID)
if err != nil {
// Terlalu verbose dan terduplikasi
wrapped := fmt.Errorf("getUserData failed: profile fetch error: inner error: %v", err)
return nil, wrapped
}
return buildUserData(user, profile), nil
}
// Hasil error message: "getUserData failed: profile fetch error: inner error: record not found"
// Redundan, sulit di-parse secara programmatic, tidak menggunakan %w sehingga
// tidak bisa di-unwrap dengan errors.Is atau errors.As
// BENAR: wrap dengan konteks yang jelas, singkat, tidak redundan, gunakan %w
func getUserData(ctx context.Context, id string) (*UserData, error) {
user, err := repo.FindUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("getUserData %s: find user: %w", id, err)
}
profile, err := repo.FindProfile(ctx, user.ProfileID)
if err != nil {
return nil, fmt.Errorf("getUserData %s: find profile: %w", id, err)
}
return buildUserData(user, profile), nil
}
// Hasil error message: "getUserData usr-123: find profile: record not found"
// Jelas, tidak redundan, bisa di-unwrap dengan errors.Is/errors.As
// Caller yang butuh cek jenis error:
if errors.Is(err, ErrNotFound) {
return http.StatusNotFound
}
Panduan error handling yang KISS di Go:
// Konvensi format error wrapping yang konsisten:
// "<nama fungsi> <identifier>: <operasi>: %w"
fmt.Errorf("createOrder %s: save to db: %w", orderID, err)
fmt.Errorf("sendEmail %s: template render: %w", userEmail, err)
fmt.Errorf("processPayment %s: charge stripe: %w", paymentID, err)
// Untuk fungsi tanpa identifier yang meaningful:
fmt.Errorf("validateConfig: missing required field 'db_host': %w", err)
// Jangan buat custom error type kecuali caller perlu membedakan jenis error:
// Tidak perlu:
type ValidationError struct{ Field string; Msg string }
// Jika caller hanya melakukan log dan return — pakai errors.New saja
// Perlu custom error type ketika:
var ErrNotFound = errors.New("not found")
var ErrUnauthorized = errors.New("unauthorized")
// Karena caller melakukan: if errors.Is(err, ErrNotFound) { ... }
KISS di Dart/Flutter #
Di Flutter, pelanggaran KISS yang paling umum adalah menggunakan state management yang berat untuk state yang bisa ditangani dengan cara yang jauh lebih sederhana.
// ANTI-PATTERN: BLoC untuk counter yang hanya dipakai di satu widget
// 5 class, 40+ baris untuk sesuatu yang butuh 5 baris
// events
abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}
class ResetEvent extends CounterEvent {}
// state
class CounterState {
final int count;
const CounterState({required this.count});
CounterState copyWith({int? count}) => CounterState(count: count ?? this.count);
}
// bloc
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(count: 0)) {
on<IncrementEvent>((e, emit) => emit(state.copyWith(count: state.count + 1)));
on<DecrementEvent>((e, emit) => emit(state.copyWith(count: state.count - 1)));
on<ResetEvent>((e, emit) => emit(const CounterState(count: 0)));
}
}
// widget
class CounterWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocBuilder<CounterBloc, CounterState>(
builder: (context, state) {
return Column(children: [
Text('${state.count}'),
ElevatedButton(
onPressed: () => context.read<CounterBloc>().add(IncrementEvent()),
child: const Text('+'),
),
]);
},
);
}
}
// BENAR: setState untuk local state yang tidak perlu dibagi
// 1 class, 20 baris, langsung jelas
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Column(children: [
Text('$_count', style: Theme.of(context).textTheme.headlineMedium),
Row(mainAxisAlignment: MainAxisAlignment.center, children: [
IconButton(
onPressed: () => setState(() => _count--),
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: () => setState(() => _count++),
icon: const Icon(Icons.add),
),
]),
]);
}
}
Panduan memilih state management di Flutter yang mengikuti KISS:
setState → state lokal, hanya relevan dalam satu widget
tidak perlu dibagi ke widget lain
tidak perlu persist setelah widget di-dispose
InheritedWidget → state yang perlu dibagi ke descendant langsung
/ Provider tanpa deep prop drilling, tapi bukan global
Riverpod / BLoC → state yang perlu:
- dibagi lintas banyak widget yang tidak related
- persist setelah widget di-dispose (cache)
- diakses dari luar widget tree (service, use case)
- ditest secara independen dari UI
BLoC, Riverpod, dan Provider adalah tools yang sangat berguna untuk use case yang tepat. Masalahnya bukan pada toolnya, tapi ketika tools berat itu digunakan untuk state yang sebenarnya bisa ditangani dengan setState — ini adalah pelanggaran KISS yang menambah kompleksitas tanpa nilai tambah.KISS dan Penamaan #
Penamaan yang terlalu abstrak adalah bentuk KISS violation yang paling halus — sekilas terlihat seperti “clean code” tapi sebenarnya menyembunyikan informasi penting.
// ANTI-PATTERN: nama yang tidak memberi informasi tentang apa yang dilakukan
type UserManager struct{} // manager of what, exactly?
type DataProcessor struct{} // process data jenis apa?
type ServiceHandler struct{} // handle service mana?
type UtilHelper struct{} // util untuk apa?
func (m *UserManager) Do(user User) error {} // Do what?
func (p *DataProcessor) Process(data interface{}) {} // produce what?
// BENAR: nama yang mencerminkan apa yang dilakukan dalam bahasa domain
type UserRegistrar struct{} // jelas: mendaftarkan user
type ProfileUpdater struct{} // jelas: mengupdate profil
type OrderFulfiller struct{} // jelas: memproses pemenuhan order
type InvoiceGenerator struct{} // jelas: membuat invoice
func (r *UserRegistrar) Register(req RegisterRequest) (*User, error) {}
func (g *InvoiceGenerator) GenerateForOrder(orderID string) (*Invoice, error) {}
// Nama variabel juga harus jelas:
// ✗
x := getUser(id)
tmp := calculateTotal(items)
result := process(data)
// ✓
currentUser := getUser(sessionID)
orderTotal := calculateTotal(order.Items)
parsedConfig := parseConfigFile(path)
Nama yang baik adalah dokumentasi yang tidak bisa ketinggalan jaman. Nama yang terlalu abstrak memaksa pembaca membuka implementasi untuk memahami kode yang menggunakannya — ini adalah beban kognitif yang tidak perlu.
Hubungan KISS dengan Prinsip Lain #
KISS tidak berdiri sendiri. Ia berinteraksi erat dengan prinsip lain dan sering menjadi penyeimbang ketika prinsip lain diterapkan secara berlebihan.
flowchart TD
KISS["KISS\n(Keep It Simple)"]
YAGNI["YAGNI\nJangan tambahkan\nyang belum dibutuhkan"]
DRY["DRY\nHilangkan duplikasi\nknowledge"]
SRP["SRP\nSatu tanggung jawab\nper komponen"]
SOLID["SOLID\nDesain yang\nbisa di-extend"]
K_Y["KISS + YAGNI\n= Jangan abstraksi\nsebelum waktunya"]
K_D["KISS + DRY\n= Abstrak hanya\nknowledge yang sama,\nbukan bentuk yang sama"]
K_S["KISS + SRP\n= Pisahkan concern,\ntapi jangan over-decompose\nhingga alur sulit diikuti"]
K_SO["KISS + SOLID\n= Gunakan pattern\nhanya saat ada\nkebutuhan nyata"]
KISS --- YAGNI --> K_Y
KISS --- DRY --> K_D
KISS --- SRP --> K_S
KISS --- SOLID --> K_SO
style KISS fill:#4C9BE8,color:#fff
style K_Y fill:#5CB85C,color:#fff
style K_D fill:#5CB85C,color:#fff
style K_S fill:#5CB85C,color:#fff
style K_SO fill:#5CB85C,color:#fffHubungan yang paling penting adalah KISS sebagai penyeimbang DRY. DRY mendorong abstraksi untuk menghilangkan duplikasi. KISS mengingatkan bahwa abstraksi yang salah lebih mahal dari duplikasi. Bersama, mereka menghasilkan keputusan yang lebih baik: abstraksi hanya ketika ada duplikasi knowledge yang nyata, dan abstraksinya sendiri harus sesederhana mungkin.
Kapan Kompleksitas Memang Dibenarkan #
KISS bukan berarti selalu pilih solusi dengan kode paling sedikit. Ada situasi di mana kompleksitas adalah harga yang pantas — bahkan wajib — dibayar.
KOMPLEKSITAS YANG DIBENARKAN:
✓ Platform atau library yang akan digunakan banyak tim berbeda
→ extensibility adalah kebutuhan nyata, bukan antisipasi
✓ Domain yang memang kompleks secara inheren
→ rules engine, workflow engine, compiler, distributed systems
→ kompleksitas ada di domain, bukan di solusi teknisnya
✓ Performance-critical code yang butuh optimasi
→ profiling menunjukkan ini adalah bottleneck nyata
→ bukan "mungkin ini lambat"
✓ Security-critical code yang butuh defense-in-depth
→ authentication, authorization, encryption
→ kompleksitas di sini adalah requirement, bukan pilihan
✓ Backward compatibility requirement
→ library publik yang tidak bisa breaking change
→ API yang sudah dipakai banyak consumer
PENDEKATAN YANG TETAP MENGIKUTI KISS:
→ Mulai sederhana
→ Ukur dan validasi kebutuhan nyata
→ Tambahkan kompleksitas secara incremental dengan justifikasi yang jelas
→ Dokumentasikan MENGAPA kompleksitas ini diperlukan, bukan hanya apa
Kompleksitas yang tidak terdokumentasi adalah utang teknis ganda. Pertama, ada biaya untuk memahaminya. Kedua, tidak ada yang tahu apakah kompleksitas itu masih relevan atau sudah bisa disederhanakan. Selalu dokumentasikan mengapa sebuah keputusan desain yang kompleks diambil — bukan hanya apa yang dilakukan.
Anti-Pattern dalam Satu Pandangan #
// ✗ Nested ternary yang sulit dibaca
status := active ? (verified ? "active_verified" : "active_unverified") : "inactive"
// ✓ If-else eksplisit, atau fungsi terpisah dengan nama yang jelas
// ✗ Method chaining panjang yang sulit di-debug
result, err := service.Load(id).Filter(active).Transform(toDTO).Validate().Save(ctx)
// ✓ Simpan setiap intermediate result — bisa di-inspect, di-log, dan di-debug
// ✗ Interface untuk satu implementasi tanpa rencana implementasi lain
type Logger interface { Log(msg string) }
type ConsoleLogger struct{}
// Tidak ada implementasi lain, tidak ada yang di-mock di test
// ✓ Langsung pakai *slog.Logger atau *zap.Logger sampai ada kebutuhan nyata
// ✗ Config struct dengan terlalu banyak field untuk use case sederhana
type ServerConfig struct {
Host, Port, ReadTimeout, WriteTimeout, IdleTimeout,
MaxHeaderBytes, MaxConns, KeepAlive, TLSCertFile, TLSKeyFile,
// ... 20 field lagi yang tidak pernah diset berbeda dari default
}
// ✓ Mulai dengan yang dibutuhkan, tambahkan saat ada kebutuhan nyata
// ✗ Nama terlalu abstrak — tidak memberi informasi domain
type Manager struct{} // Manager of what?
type Handler struct{} // Handles what?
type Processor struct{} // Processes what?
// ✓ UserRegistrar, PaymentCharger, InvoiceGenerator — nama yang mencerminkan domain
// ✗ Pass-through layer tanpa nilai tambah
type UserUseCase struct{ repo UserRepository }
func (u *UserUseCase) FindByID(id string) (*User, error) {
return u.repo.FindByID(id) // hanya meneruskan panggilan, tidak ada logic
}
// ✓ Hapus layer ini; atau tambahkan logic nyata (validasi, transform, caching)
// ✗ Generic yang terlalu dini
type Result[T any] struct{ Value T; Err error }
// Jika selalu dipakai sebagai Result[UserData] saja — tidak ada nilai generiknya
// ✓ Kembalikan (*UserData, error) langsung — idiomatis Go
Checklist Review KISS #
FUNGSI DAN METHOD:
□ Setiap fungsi bisa dijelaskan dalam satu kalimat tanpa kata "dan"
□ Tidak ada nested condition lebih dari dua level — gunakan guard clause
□ Tidak ada chaining panjang yang menghalangi debugging
□ Nama mencerminkan apa yang dilakukan, bukan hanya tipe atau kategori
ABSTRAKSI:
□ Setiap interface punya lebih dari satu implementasi yang nyata
(atau setidaknya digunakan untuk mocking di unit test)
□ Tidak ada layer yang hanya meneruskan panggilan tanpa logic
□ Tidak ada generic type yang hanya dipakai dengan satu tipe konkret
□ Tidak ada abstraksi untuk kebutuhan "mungkin suatu hari nanti"
API DAN KONTRAK:
□ Endpoint menggunakan HTTP method yang semantically benar
□ Schema input/output jelas dan terdokumentasikan
□ Error message jelas, tidak redundan, dan bisa di-trace ke sumbernya
STATE MANAGEMENT (Flutter/Dart):
□ setState hanya untuk local state satu widget
□ State management berat hanya untuk state yang perlu dibagi lintas widget
□ Pilihan state management bisa dijelaskan dengan alasan konkret
KOMPLEKSITAS:
□ Setiap keputusan desain yang kompleks terdokumentasikan alasannya
□ Tidak ada "fleksibilitas masa depan" tanpa stakeholder konkret yang memintanya
□ Engineer baru bisa memahami alur utama dalam <15 menit
Ringkasan #
- KISS bukan kode yang naif atau ceroboh — tapi kode yang jelas, eksplisit, dan tidak lebih kompleks dari yang diperlukan. Perbedaannya: sederhana masih punya error handling dan edge case yang benar; naif mengabaikannya.
- Tujuh tanda over-engineering yang perlu diwaspadai: framework internal lebih besar dari business logic, butuh >5 menit menjelaskan alur sederhana, interface tanpa implementasi kedua, pass-through layer, generic untuk satu tipe, extensibility tanpa roadmap, dan “fleksibilitas masa depan” yang tidak konkret.
- Guard clause adalah teknik KISS paling efektif: validasi semua precondition di awal dengan early return, biarkan logic utama berjalan di level teratas tanpa nesting. Setiap level nesting tambahan meningkatkan cognitive load secara eksponensial.
- Abstraksi prematur adalah pelanggaran KISS paling umum di kalangan engineer berpengalaman. Mulai konkret, ekstrak abstraksi hanya saat ada kebutuhan nyata — bukan antisipasi.
- API eksplisit lebih KISS dari endpoint generic: mudah didokumentasikan, di-secure per route, di-cache, dan dipahami tanpa membaca dokumentasi panjang.
- Error handling yang KISS: wrap dengan konteks singkat dan tidak redundan, gunakan
%wagar bisa di-unwrap, buat custom error type hanya ketika caller perlu membedakan jenis error.- Di Flutter:
setStateuntuk local state, BLoC/Riverpod untuk state yang benar-benar perlu dibagi. Pilih berdasarkan kebutuhan nyata, bukan kebiasaan.- KISS + YAGNI mencegah premature abstraction; KISS + DRY menghasilkan keseimbangan: abstraksi ketika ada duplikasi knowledge nyata, bukan duplikasi bentuk kode.
- Kompleksitas yang dibenarkan memang ada — platform publik, domain yang inheren kompleks, performance-critical, security-critical. Tapi selalu dokumentasikan mengapa kompleksitas itu diperlukan, dan mulai sederhana sebelum menambahnya.