Lima Kesalahan "Sepele" yang Bikin Codebase Ruwet
Lima Kesalahan "Sepele" yang Bikin Codebase Ruwet
Di balik layar sistem yang berjalan mulus, sering kali tersembunyi sebuah kode yang ruwet—bukan karena algoritma yang canggih, tapi karena kelalaian-kelalaian dasar yang terakumulasi.
Saya baru saja menyelesaikan review untuk sekitar tiga ratus baris kode dari tim junior. Bukan kode yang kompleks. Hanya CRUD biasa, beberapa validasi, sedikit logika bisnis. Tapi setelah membacanya, kepala saya terasa seperti habis diaduk. Bukan karena susah memahaminya, tapi karena cara pemahaman itu didapat: dengan usaha ekstra, dengan tebak-tebakan, dengan bolak-balik ke bagian lain untuk mencari konteks. Kodenya jalan? Jalan. Lolos testing? Lolos. Tapi ada sesuatu yang salah. Sesuatu yang membuat kode ini terasa seperti rumah yang dibangun tanpa denah: semua barang ada, tapi untuk mencari sendal saja kamu harus membongkar tiga lemari.
Ini adalah kerja sunyi yang sering luput: membaca kode bukan untuk mencari bug, tapi untuk mencari *rasa*. Rasa apakah kode ini mudah dipahami oleh orang lain—atau oleh dirimu sendiri enam bulan lagi. Dan dari ratusan baris itu, saya menemukan pola. Lima kesalahan yang terlihat sepele, tapi seperti batu kerikil di sepatu: jika cuma satu, bisa diabaikan; jika terkumpul ratusan, kamu tidak bisa berjalan.
1. Penamaan yang Malas (atau Sok Pintar)
Ini pelakunya yang paling sering. Variabel bernama a, temp, data, result. Atau yang lebih buruk: flag. Flag apa? Flag untuk menandai apakah user sudah login? Flag untuk status pembayaran? Flag untuk apa? Otak yang membaca harus mundur beberapa langkah untuk mencari tahu dari konteks.
Contoh nyata dari kode tadi:
if (flag) { process(data); }
Apa artinya? Saya harus scroll ke atas untuk mencari di mana flag di-set. Ternyata: let flag = user.role === 'admin' && item.status === 'pending';. Dua puluh baris di atas. Kenapa tidak langsung ditulis:
const isAdminAndItemPending = user.role === 'admin' && item.status === 'pending';
if (isAdminAndItemPending) { process(item); }
Sekarang, orang yang baca tidak perlu scroll. Mereka langsung tahu. Penamaan yang baik adalah dokumentasi gratis. Ia mengurangi *cognitive load*. Setiap kali kamu menulis temp, kamu memaksa otak orang lain (atau futurumu) untuk melakukan sedikit kerja ekstra. Kumpulkan sedikit-sedikit, jadi beban besar.
2. Fungsi yang Mencoba Melakukan Segalanya
Saya menemukan sebuah fungsi bernama handleUserAction(). Panjangnya 120 baris. Di dalamnya, dia: (1) validasi input, (2) query database, (3) kalkulasi harga, (4) kirim email notifikasi, (5) update log, (6) kirim response. Semua dalam satu fungsi raksasa.
Masalahnya bukan panjangnya. Masalahnya adalah *tanggung jawab* yang monolitis. Jika ada bug di pengiriman email, kamu harus masuk ke fungsi besar ini dan memilah-milah. Jika logika kalkulasi berubah, kamu harus menyentuh fungsi yang juga berurusan dengan email dan database. Ini melanggar prinsip Single Responsibility. Fungsi seperti ini adalah bom waktu. Suatu hari, perubahan kecil di satu bagian akan merusak bagian lain yang tidak terkait, dan debugging-nya akan jadi mimpi buruk.
Kerja sunyinya adalah memotong-motong. Buat fungsi kecil: validateInput(), calculatePrice(), sendNotificationEmail(). Lalu panggil mereka dari handleUserAction(). Sekarang, setiap bagian bisa di-test sendiri, dipahami sendiri, diubah sendiri. Kode jadi seperti modul LEGO, bukan seperti gumpalan tanah liat.
3. Magic Number dan String yang Tersebar
Di satu file, ada angka 86400000. Di file lain, ada string 'COMPLETED'. Di file lain lagi, ada 3 sebagai batas maksimal percobaan. Ini adalah *magic values*: nilai yang punya makna penting, tetapi maknanya tidak tertulis di dekatnya.
setTimeout(() => { checkStatus(); }, 86400000);
Apa itu 86400000? Saya harus menghitung: 1000ms * 60 detik * 60 menit * 24 jam = 86.400.000. Oh, itu satu hari. Tapi apakah semua orang yang baca akan menghitung? Atau cukup tulis:
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
setTimeout(() => { checkStatus(); }, ONE_DAY_IN_MS);
Sekarang jelas. Hal yang sama untuk status: jangan tulis if (status === 'COMPLETED') di sepuluh tempat berbeda. Buat sebuah konstanta atau enum: const STATUS = { COMPLETED: 'COMPLETED', PENDING: 'PENDING' };. Lalu gunakan STATUS.COMPLETED. Jika suatu hari nilai string-nya perlu berubah (misal jadi 'DONE'), kamu hanya ubah di satu tempat.
4. Error Handling yang Hiasan
Ini favorit saya. Blok try-catch yang isinya hanya console.log(error). Atau, lebih parah, kosong.
try {
saveToDatabase(data);
} catch (err) {
console.log('Error saving data:', err);
}
Lalu? Aplikasi terus berjalan seolah tidak terjadi apa-apa. Data tidak tersimpan, tapi user dapat notifikasi "sukses". Error handling semacam ini lebih berbahaya daripada tidak ada handling sama sekali, karena memberi ilusi bahwa semuanya aman. Kerja sunyi yang penting adalah memutuskan: apa yang harus terjadi ketika error? Apakah operasi harus diulang? Apakah user harus dapat pesan error yang jelas? Apakah sistem harus fallback ke mekanisme lain? console.log bukan solusi; itu adalah pengakuan bahwa kita tidak mau repot.
5. Komentar yang Menjelaskan *Apa*, Bukan *Mengapa*
Komentar seperti ini sering ditemukan:
// increment i
i++;
Atau:
// get user by id
const user = getUserById(id);
Komentar ini tidak berguna. Kode sudah menjelaskan *apa* yang dilakukan. Yang sering hilang adalah *mengapa*. Kenapa kita increment i di titik ini? Kenapa kita perlu get user by id dari cache, bukan dari database? Konteks itulah yang hilang.
Komentar yang baik menjelaskan hal yang tidak terlihat dari kode:
// Start from index 1 because the first element is a header row
for (let i = 1; i < rows.length; i++) {
Atau:
// Use cached version here because this function is called frequently in a loop,
// and database latency would become a bottleneck.
const user = getCachedUserById(id);
Komentar adalah cerita untuk developer berikutnya. Ceritakan hal yang penting, bukan hal yang sudah jelas.
Lima kesalahan ini tidak akan menyebabkan sistem crash hari ini. Tidak akan muncul di monitoring. Tidak akan dikeluhkan user. Mereka adalah kebisingan latar belakang yang perlahan-lahan meningkatkan *friction* dalam pengembangan. Setiap kali developer baru masuk ke proyek, mereka butuh waktu ekstra untuk memahami kode. Setiap kali ada bug, waktu debugging membengkak karena harus melacak logic yang berantakan. Setiap kali ada fitur baru, risiko regresi meningkat karena kode terlalu saling terkait.
Kerja sunyi sejati adalah membangun disiplin untuk menghindari hal-hal "sepele" ini. Itu berarti meluangkan waktu 2 menit ekstra untuk memberi nama yang jelas. Itu berarti berani menolak menambah logika baru ke fungsi yang sudah gemuk, dan memilih refactor kecil. Itu berarti tidak puas dengan kode yang "jalan saja", tetapi mengejar kode yang *jelas*.
Efeknya tidak dramatis. Tidak ada yang akan memberimu pujian karena mengganti flag dengan isEligibleForDiscount. Tapi dalam enam bulan, ketika timmu bisa menambah fitur baru dalam dua hari bukan dua minggu, ketika bug baru bisa dilacak dalam sejam bukan seharian, barulah kerja sunyi itu terbayar. Ia terbayar dalam kecepatan, dalam ketenangan, dan dalam kualitas sistem yang tidak rewel.
Dan dalam kerja sunyi sistem, mengurus hal-hal dasar seperti penamaan dan error handling inilah yang sebenarnya menentukan apakah kita akan menghabiskan waktu untuk *membangun* atau sekadar *memperbaiki*.
Q&A dari Sesama yang Merasakan Keruwetan
Q: Apakah semua magic number harus di-constant? Bagaimana dengan angka 0 atau 1?
A> Angka 0 dan 1 seringkali punya makna jelas dalam konteks loop atau kondisi (start index, increment). Tapi tanyakan: apakah maknanya benar-benar jelas? if (retryCount > 3) lebih jelas daripada if (retryCount > MAX_RETRY_ATTEMPTS)? Seringkali iya. Tapi jika angka itu adalah batas bisnis (misal: maksimal 3 kali coba), menyimpannya dalam konstanta MAX_RETRY mencegah jika suatu hari bisnis bilang "ubah jadi 5".
Q: Fungsi kecil-kecil bukannya bikin file jadi banyak dan susah dilacak?
A> Iya, itu trade-off. Tapi susah dilacak karena apa? Karena penamaan yang buruk. Jika kamu punya fungsi bernama calculateDiscountedPrice(), validateCustomerVoucher(), dan sendOrderConfirmation(), justru lebih mudah dilacak dan dipahami daripada satu fungsi processOrder() yang panjang. Pengelompokan ke dalam modul atau class yang logis bisa mengatur banyaknya file.
Q: Bagaimana melatih kebiasaan penamaan yang baik?
A> Latihan sederhana: tulis kode, lalu coba baca besoknya. Jika kamu harus berpikir lebih dari 2 detik untuk memahami apa yang dilakukan sebuah variabel atau fungsi, ganti namanya. Gunakan kata kerja untuk fungsi (calculate, validate, transform), dan boolean diawali is, has, can (isActive, hasPermission).
Q: Error handling yang ideal seperti apa?
A> Tergantung lapisan. Di layer business logic, error harus ditangkap, dicatat dengan konteks yang jelas (menggunakan structured logging), dan dikonversi ke error type yang sesuai untuk dikonsumsi layer atas (misal, throw ValidationError atau DatabaseError). Di layer controller/API, tangkap error itu dan berikan response HTTP yang tepat (400, 500, dll) dengan pesan yang aman (tidak expose detail internal). Jangan pernah menelan error begitu saja.
Q: Apakah komentar diperlukan jika kita punya dokumentasi?
A> Dokumentasi sering ketinggalan. Kode adalah sumber kebenaran. Komentar dalam kode adalah dokumentasi yang paling dekat dengan implementasi. Tujuannya bukan untuk menduplikasi kode, tapi untuk memberikan *intensi* dan *konteks* yang tidak tertulis dalam sintaks.
Q: Kapan waktu yang tepat untuk refactor kode ruwet seperti ini?
A> Ada dua strategi: (1) Boy Scout Rule: setiap kali kamu menyentuh sebuah file, tinggalkan lebih bersih daripada kamu temukan. Ganti satu nama variabel, pecah satu fungsi. (2) Refactor dedikasi: saat kamu akan menambah fitur besar di area tersebut, luangkan 10-30% waktu untuk membersihkan dulu. Jangan pernah refactor besar-besaran tanpa alasan yang terkait dengan fitur/bug, itu berisiko tinggi.
Q: Apa tools yang bisa membantu mendeteksi masalah-masalah "sepele" ini?
A> Linter (ESLint untuk JS, Pylint untuk Python, dll) bisa menangkap banyak hal: nama variabel yang buruk, fungsi terlalu panjang, magic number. Static code analysis tools seperti SonarQube juga membantu. Tapi tools hanya alat. Kesadaran dan disiplin tim adalah kuncinya.
Five "Trivial" Mistakes That Make a Codebase Messy
Behind the scenes of a smoothly running system, there often lies messy code—not because of sophisticated algorithms, but due to accumulated basic oversights.
I just finished reviewing about three hundred lines of code from a junior team. It wasn't complex code. Just ordinary CRUD, some validations, a bit of business logic. But after reading it, my head felt like it had been stirred. Not because it was hard to understand, but because of the way that understanding was achieved: with extra effort, with guesswork, with jumping back and forth to other parts to find context. Did the code run? Yes. Pass testing? Yes. But something was wrong. Something that made this code feel like a house built without a blueprint: everything is there, but to find your slippers you have to dig through three cabinets.
This is the quiet work that often gets missed: reading code not to find bugs, but to find its *feel*. The feel of whether this code is easy to understand by others—or by yourself six months later. And from those hundreds of lines, I found a pattern. Five mistakes that seem trivial, but like pebbles in a shoe: if there's only one, it can be ignored; if hundreds accumulate, you can't walk.
1. Lazy (or Overly Clever) Naming
This is the most frequent culprit. Variables named a, temp, data, result. Or worse: flag. Flag for what? Flag to mark if the user is logged in? Flag for payment status? Flag for what? The reading brain has to backtrack several steps to figure it out from context.
A real example from the code earlier:
if (flag) { process(data); }
What does it mean? I had to scroll up to find where flag was set. It turned out: let flag = user.role === 'admin' && item.status === 'pending';. Twenty lines above. Why not just write:
const isAdminAndItemPending = user.role === 'admin' && item.status === 'pending';
if (isAdminAndItemPending) { process(item); }
Now, anyone reading doesn't need to scroll. They immediately know. Good naming is free documentation. It reduces *cognitive load*. Every time you write temp, you force someone else's brain (or your future self) to do a little extra work. Collect those little bits, and they become a heavy burden.
2. Functions That Try To Do Everything
I found a function named handleUserAction(). It was 120 lines long. Inside, it: (1) validated input, (2) queried the database, (3) calculated price, (4) sent notification email, (5) updated a log, (6) sent a response. All in one monolithic function.
The problem isn't its length. The problem is its monolithic *responsibility*. If there's a bug in email sending, you have to dive into this big function and sift through. If the calculation logic changes, you have to touch the function that also deals with email and database. This violates the Single Responsibility Principle. Functions like this are time bombs. One day, a small change in one part will break another unrelated part, and debugging it will be a nightmare.
The quiet work is slicing it up. Make small functions: validateInput(), calculatePrice(), sendNotificationEmail(). Then call them from handleUserAction(). Now, each part can be tested, understood, and changed independently. The code becomes like LEGO modules, not a lump of clay.
3. Magic Numbers and Strings Scattered Everywhere
In one file, there's the number 86400000. In another, the string 'COMPLETED'. In yet another, 3 as a maximum retry limit. These are *magic values*: values with important meaning, but that meaning isn't written near them.
setTimeout(() => { checkStatus(); }, 86400000);
What is 86400000? I had to calculate: 1000ms * 60 seconds * 60 minutes * 24 hours = 86,400,000. Oh, it's one day. But will everyone who reads this calculate? Or just write:
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
setTimeout(() => { checkStatus(); }, ONE_DAY_IN_MS);
Now it's clear. Same for status: don't write if (status === 'COMPLETED') in ten different places. Create a constant or enum: const STATUS = { COMPLETED: 'COMPLETED', PENDING: 'PENDING' };. Then use STATUS.COMPLETED. If one day the string value needs to change (e.g., to 'DONE'), you only change it in one place.
4. Cosmetic Error Handling
This is my favorite. try-catch blocks that only contain console.log(error). Or, worse, are empty.
try {
saveToDatabase(data);
} catch (err) {
console.log('Error saving data:', err);
}
And then? The application continues as if nothing happened. Data isn't saved, but the user gets a "success" notification. This kind of error handling is more dangerous than having none at all, because it gives the illusion that everything is safe. The important quiet work is deciding: what should happen when an error occurs? Should the operation be retried? Should the user get a clear error message? Should the system fall back to another mechanism? console.log is not a solution; it's an admission that we can't be bothered.
5. Comments That Explain *What*, Not *Why*
Comments like these are often found:
// increment i
i++;
Or:
// get user by id
const user = getUserById(id);
These comments are useless. The code already explains *what* is being done. What's often missing is the *why*. Why do we increment i at this point? Why do we need to get the user by id from cache, not the database? That context is what's missing.
Good comments explain things not visible from the code:
// Start from index 1 because the first element is a header row
for (let i = 1; i < rows.length; i++) {
Or:
// Use cached version here because this function is called frequently in a loop,
// and database latency would become a bottleneck.
const user = getCachedUserById(id);
Comments are stories for the next developer. Tell the important parts, not the obvious ones.
These five mistakes won't cause a system crash today. They won't appear in monitoring. Users won't complain. They are background noise that gradually increases the *friction* in development. Every time a new developer joins the project, they need extra time to understand the code. Every time there's a bug, debugging time balloons because they have to trace messy logic. Every time a new feature is added, regression risk increases because the code is too intertwined.
The true quiet work is building the discipline to avoid these "trivial" things. That means spending an extra 2 minutes to give a clear name. It means having the courage to refuse adding new logic to an already bloated function, and choosing a small refactor instead. It means not being satisfied with code that "just works," but pursuing code that is *clear*.
The effect isn't dramatic. No one will praise you for changing flag to isEligibleForDiscount. But in six months, when your team can add a new feature in two days instead of two weeks, when a new bug can be traced in an hour instead of a day, that's when the quiet work pays off. It pays off in speed, in peace of mind, and in the quality of a system that isn't fussy.
And in the quiet work of the system, taking care of basic things like naming and error handling is what truly determines whether we will spend our time *building* or merely *fixing*.
Q&A from Fellow Sufferers of Messiness
Q: Do all magic numbers need to be constants? What about 0 or 1?
A> The numbers 0 and 1 often have clear meaning in the context of loops or conditions (start index, increment). But ask: is their meaning truly clear? if (retryCount > 3) clearer than if (retryCount > MAX_RETRY_ATTEMPTS)? Often, yes. But if that number is a business limit (e.g., max 3 attempts), storing it in a constant MAX_RETRY prevents issues if the business one day says "change it to 5".
Q: Won't many small functions make files numerous and hard to track?
A> Yes, that's a trade-off. But hard to track because of what? Bad naming. If you have functions named calculateDiscountedPrice(), validateCustomerVoucher(), and sendOrderConfirmation(), they are actually easier to track and understand than one long processOrder() function. Grouping them into logical modules or classes can manage the number of files.
Q: How to train good naming habits?
A> Simple exercise: write code, then try to read it tomorrow. If you have to think for more than 2 seconds to understand what a variable or function does, rename it. Use verbs for functions (calculate, validate, transform), and prefix booleans with is, has, can (isActive, hasPermission).
Q: What is ideal error handling?
A> It depends on the layer. In the business logic layer, errors should be caught, logged with clear context (using structured logging), and converted to appropriate error types for consumption by upper layers (e.g., throw ValidationError or DatabaseError). In the controller/API layer, catch those errors and provide appropriate HTTP responses (400, 500, etc.) with safe messages (don't expose internal details). Never silently swallow errors.
Q: Are comments needed if we have documentation?
A> Documentation often falls behind. Code is the source of truth. Comments in code are the documentation closest to the implementation. Their purpose is not to duplicate code, but to provide *intent* and *context* not written in the syntax.
Q: When is the right time to refactor messy code like this?
A> Two strategies: (1) Boy Scout Rule: every time you touch a file, leave it cleaner than you found it. Change one variable name, split one function. (2) Dedicated refactor: when you're about to add a major feature to that area, allocate 10-30% of the time to clean it up first. Never do a massive refactor without a reason related to a feature/bug, it's high risk.
Q: What tools can help detect these "trivial" issues?
A> Linters (ESLint for JS, Pylint for Python, etc.) can catch many things: bad variable names, overly long functions, magic numbers. Static code analysis tools like SonarQube also help. But tools are just instruments. Team awareness and discipline are the key.
Thank you for stopping by! If you enjoy the content and would like to show your support, how about treating me to a cup of coffee? �� It’s a small gesture that helps keep me motivated to continue creating awesome content. No pressure, but your coffee would definitely make my day a little brighter. ☕️ Buy Me Coffee

Post a Comment for "Lima Kesalahan "Sepele" yang Bikin Codebase Ruwet"
Post a Comment
You are welcome to share your ideas with us in comments!