Engineering

5 Worst Practices That Made Me a Better Engineer

September 9, 2026 · 8 min read

There's an old line from Jack Ma that's stuck with me for years: "What's the difference between smart and wisdom? Smart people know what they want. Wise people know what they don't want."

Smart engineers chase every shiny idea and forget the consequences. Wise ones know where to stop. I've spent a good chunk of my career learning that the hard way — through outages, angry Slack threads, and a few 2 AM "why is production down" moments.

During my five years on the Bukalapak engineering team, I collected a small museum of mistakes. Some were harmless. A few were genuinely painful. Here are the five that changed how I build software for good.

1. Rolling out too many things at once

Late 2017. We were building a big promotional feature for Ramadhan 2018, and back then our team had no idea how to ship incrementally. So we did what felt natural at the time: opened one branch, dumped every change into it, and let it live inside a monolith with 300–400 other engineers actively pushing code.

That was mistake number one. Every day brought new merge conflicts. The branch kept growing. By the time we were ready for review, we were asking colleagues to wade through roughly 5,000 lines of change across 237 files. Put yourself in a reviewer's shoes for a second — what goes through your head staring down a diff that size? "Ok, LGTM, let's just ship it"? "I don't have the energy to actually read this, auto-approve"? Neither answer is good, and both happen more than anyone admits.

Review dragged on for two weeks. The deadline crept closer. And when we finally deployed, the changes broke things in production — and rolling back wasn't even an option, so we had to fix forward under pressure. We hit the deadline, barely. But the pain point was that we can't rollback, we can't isolate bugs, and we couldn't get any feedback fast.

What fixed it:

  • Trunk-Based Development — ship in small, frequent chunks instead of one giant living branch. It took real habit-building to learn how to slice requirements this small, but it's non-negotiable now.
  • Release toggles — ship code dark, flip it on when you're confident, flip it off instantly if something breaks. No redeploy required.
  • Gradual rollout — release to a slice of traffic first, watch it, then expand. Confidence should be earned in production, not assumed.

And just as important: negotiate scope with product up front. Define the real MVP, propose alternatives that cost less to build, and never quietly agree to an unrealistic timeline just to avoid an awkward conversation.

2. Work harder, not smarter

I used to believe grinding harder was the way to stand out — overtime by default, always visibly "busy." What I wasn't tracking was leverage: the impact you produce relative to the time you spend. A senior engineer with 3x the impact of a junior doesn't work 3x the hours. The math doesn't work that way, and chasing it is a trap.

The clearest example: we were asked to apply a discount to roughly 300 million products. We had no bulk-subsidy feature, so we hacked one together and wrote a script that could only touch 150,000 products per run, each run taking ~12 hours. Do the math — that's 2,000 runs, or about 1,000 days, to cover the catalog. We ran it anyway, applied discounts to 3–5 million products, watched the metrics barely move, and then had to reverse the whole thing.

All that effort for basically nothing. The better answer, obvious in hindsight: hardcode the discount logic at the payment layer and gate it with a toggle. Minutes of setup instead of months of script babysitting.

The second case: migrating a high-traffic feature to a microservice, we copy-pasted the old caching setup — Redis, Memcached, and Varnish — because "that's what worked before." Nobody asked why three. Turns out our API Gateway already had built-in caching we could've configured with a single YAML file.

Working harder without checking for leverage costs you in four ways: an unbalanced risk-to-reward ratio, wasted effort on solutions that get shut down anyway, burnout from ignoring work-life balance, and — the sneaky one — toxic productivity, where you start resenting teammates who achieve the same outcome with less visible strain.

What I do differently now: look for the simpler solution first, check if existing tooling already solves the problem, and challenge every requirement with real questions — does this actually move the metric we care about? What's the data behind this ask?

3. Cargo cult

Cargo culting is doing something because it "worked before," without ever asking why it worked. I was guilty of this constantly. That three-cache-platform decision from the previous section? I never bothered to learn that Redis is single-threaded and Memcached is multi-threaded, or that Memcached only stores key-value pairs while Redis handles richer data structures. We copied a pattern blind, and it quietly degraded performance instead of improving it.

The moment this really landed for me was during an incident. I jumped to a root cause too fast, presented it with confidence, and it was wrong. My manager's feedback was blunt: that kind of mistake is expensive — not just the wasted fix, but the financial cost of the unresolved issue sitting there while we chased ghosts.

The fix became a habit:

  1. Actually understand the requirement before reaching for a familiar pattern.
  2. Do the deep-dive — don't assume "it worked there" means "it'll work here."
  3. Write a short technical doc before implementing, so the reasoning exists somewhere other than your head.
  4. Run it by peers. If your understanding is wrong, better to find out before you ship it.

4. Testing in production

"Real men test in production" is a meme for a reason — and honestly, as systems scale, some amount of production testing becomes unavoidable. You can't manually verify a hundred features by hand forever; automated tests in your CI/CD pipeline are what make that sustainable. The problem isn't testing in production — it's testing in production carelessly.

Case in point: back in 2018, without a staging environment that mirrored production load, we stress-tested our promotional feature directly on prod. Every couple of nights, 1 AM to 5 AM, we'd hammer it until it fell over. Except our "dummy" test products were visible to real customers — including a pencil box helpfully priced at Rp1,000,000. Customers tried to buy things that got cancelled mid-checkout. Confusing, and avoidable.

Separately, I once ran a heavy analytical query — the kind that belongs in a data warehouse — directly against the beta database. It timed out. I tried again. And again. Until someone from infra messaged the team asking who was hammering the database with data-science-style queries. I nearly took the whole thing down.

Testing in production responsibly means:

  • Limit blast radius with whitelisting — only the people who need to see the change, see it.
  • Automate what you can, and validate the automation before it enters your pipeline.
  • Work from a checklist so you're not relying on memory under pressure.
  • Document dependencies so a failure is fast to trace.
  • Never test alone — a second set of eyes catches what tunnel vision misses.

So maybe the meme just needs one more word: "Real engineers test in production — properly."

5. Over future-thinking

The opposite failure mode: over-engineering for a future that never shows up. In requirement discussions, it's easy to keep adding "but what if we need X later" until the scope balloons and the actual deadline suffers. Some of that instinct is healthy — security and privacy concerns genuinely deserve it. Most of the time, it's just scope creep wearing a smart-sounding disguise.

I've been the person pitching "reusable, general-purpose" solutions for problems nobody asked to solve yet. Thankfully, I've worked with teams critical enough to push back — "do we actually need this now, or can it wait?" Without that pushback, deadlines get eaten by work nobody asked for, and the thing you were actually supposed to ship gets less attention than it deserved.

Why it backfires:

  • You lose sight of the actual goal chasing hypothetical improvements.
  • Plans get sloppy because there are too many half-formed ideas competing for attention.
  • Speculative work often becomes tech debt nobody remembers the reasoning for.
  • You end up solving problems that may never occur.

What keeps me honest: lock the scope early and defend it, speak up the moment an idea starts sounding "nice to have someday" rather than "needed now," and try to see the feature from the customer's seat — if this improvement didn't exist, would they even notice?


None of this was fixed overnight. It took a team willing to give honest feedback, a manager who called out the false root-cause moment instead of letting it slide, and a culture at Bukalapak that treated mistakes as something to learn from instead of something to hide — "try, fail, and try again," not "don't ever fail."

Mistakes are unavoidable. The only real failure is making the same one twice. As cliché as it sounds, what actually makes us better engineers — and better humans — is staying willing to learn from what went wrong.