DEV Community

Art light
Art light

Posted on

When Code Fails, Careers Don’t Have To: Engineering Strategies for Recovering Smarter

A few years ago, I pushed a change that looked completely safe.

The task was simple: improve the performance of an API that was getting slower as traffic increased. The endpoint was pulling user data, transaction history, permissions, and a few calculated fields. Locally it looked fine. Tests passed. The response time dropped from around 900ms to 250ms. I merged it, deployed it, watched the first few requests succeed, and moved on. About forty minutes later, production started falling apart. Database CPU climbed above 90 percent. Connections filled up. Requests that had taken 250ms were now taking five or six seconds. Then they started timing out completely. The optimization had reduced latency for one request while multiplying the number of database queries under real concurrency. I had made the code faster and the system worse.

That incident changed the way I think about code failure. A failed build is not the dangerous part. A bad deployment is not even the dangerous part. The real danger is treating failure as something separate from engineering. Good developers do not avoid failure forever. They build systems that make failure visible, limited, reversible, and understandable. In my case, the first mistake was testing performance with one user. Production had hundreds of requests arriving together. An innocent-looking loop triggered repeated database calls and created an N+1 query problem. Under light traffic it was invisible. Under load it became expensive very quickly. The fix was not clever. I replaced repeated lookups with batched queries, added the right indexes, introduced request-level caching, and then ran load tests that simulated real concurrency instead of clicking the endpoint a few times and calling it fast.

The second lesson came from memory. We had a worker processing large data files. It behaved perfectly during development because our sample files were small. In production, one customer uploaded several gigabytes. The worker loaded the entire file into memory, transformed it, created another full copy, and then serialized the result. Memory climbed until the container was killed. Kubernetes restarted it, the job was picked up again, and the exact same thing happened. We had accidentally built a restart loop. The solution was to stop thinking about the file as one object. We changed the pipeline to stream data in chunks, applied backpressure, moved intermediate results out of process memory, and added limits around job size and execution time. More importantly, we added metrics around heap usage, queue depth, job duration, retry counts, and container restarts. The bug mattered, but the lack of visibility had made it much more expensive.

Another failure involved a distributed payment workflow. A request created a payment, wrote a record to the database, published an event, and triggered downstream processing. Most of the time everything worked. Then a network timeout appeared between two services. The caller did not know whether the first request had succeeded, so it retried. The service processed the request twice. That is where the word "retry" stopped sounding harmless to me. In distributed systems, retries without idempotency can become duplicate writes, duplicate emails, duplicate blockchain transactions, or duplicate payments. We introduced idempotency keys, unique database constraints, transaction boundaries, and a state machine that made every transition explicit. We also changed our message consumers so they assumed an event could arrive more than once. Exactly-once processing sounds attractive, but in many real systems it is safer to build operations that can survive being executed repeatedly.

Security failures teach the same lesson more aggressively. One project accepted user-controlled URLs for an integration service. The code validated the string, made an HTTP request, and returned parsed metadata. It looked like a normal feature until someone pointed out that the server could potentially be convinced to request internal addresses. That small utility was close to becoming an SSRF problem. Another service built SQL dynamically because the developer needed flexible filtering. Another application logged complete request objects, including authentication headers, because debugging was easier that way. None of these failures came from developers who did not know how to code. They came from solving the immediate problem without considering where untrusted data crossed system boundaries. The strategy became simple: validate inputs at boundaries, parameterize database queries, separate public and internal networks, restrict outbound access, never place secrets in logs, use least-privilege credentials, rotate keys, and design APIs assuming somebody will eventually send input you never expected.

The most useful change I made was not learning another framework. It was changing the way I approach risky code. Before implementing something important, I now ask a few questions. What happens when this runs 1,000 times at once? What happens when the database is slow? What happens if the request is delivered twice? What happens if the process dies halfway through? What happens if an attacker controls this field? What happens when the dependency returns malformed data? What happens if I need to undo this deployment in five minutes? Those questions lead naturally to better engineering: load testing, timeouts, circuit breakers, idempotency, feature flags, migrations that support rollback, structured logging, tracing, metrics, alerts, staged deployments, and smaller changes. Code failure is not automatically career failure. Repeating invisible failures without changing how you build systems can become one. The strongest engineers I have worked with are not the people whose code never breaks. They are the people who can understand a failure, reduce its blast radius, repair the system, and make the same class of failure harder to repeat.

Top comments (1)

Collapse
 
divyasinghdev profile image
Divya

Honestly, as i am learning more, building more, getting more feedback, this article i realize, is so true.

Moving fast is ok, but we gotta make systems that can survive & last so many things.