The Pragmatic Delay Strategy in Software Development
There is a decision-making pattern I use constantly that sounds like a joke but has saved me from months of wasted engineering effort. It goes like this: before building the fancy abstraction, the robust system, or the optimized solution, ask yourself whether you can honestly run the current thing until it breaks. Not forever. Until it breaks. Then fix it. I first learned this the hard way. I spent three weeks building a custom caching layer for an internal tool that processed maybe forty records a day. It had Redis, invalidation logic, and everything. Six months later, we still process forty records a day. The cache was the most expensive thing I built that year. The naive version would have taken me twenty minutes to write and worked fine.
Can I Do It Till I Need Glasses
This is basically a mnemonic version of the principle: can I ship the simple thing and only add complexity when the simple thing actually fails? The "glasses" part is just making the moment concrete — that point where the cost of not having the optimization outweighs the cost of having built it prematurely. The mechanism works like this. You ship the simplest possible implementation. You watch it. You measure actual numbers instead of guessing. When something breaks or slows down, you fix that specific thing. Sometimes that's it. Sometimes the fix cascades into building the more elaborate version you would have shipped initially, but now you have real data telling you exactly what it needs to handle. I use this approach on several recurring decisions. Pagination is the classic one. Do I build proper offset-based pagination for a query that returns eight results? No. I return all results. I move on. Same with error handling for edge cases that have never happened in production. Same with database indexing before I know the query patterns. Same with abstraction layers around something I might replace entirely in six months anyway.
Here is where people get it wrong. The approach is not an excuse to write bad code permanently. There is a difference between deferring a decision and avoiding it. If you know a bottleneck is coming and you deliberately ship technical debt without a plan, that is just negligence. The key is honest assessment of whether the problem is actually likely to materialize. I keep a personal checklist. First, I ask what the simplest working version looks like and whether it meets today's requirements. Then I look at the growth trajectory. If a service handles two requests per minute and the business case for a tenfold increase does not exist, I am not building for that increase. If a feature is internal-only and unlikely to scale beyond five users, the simplest version is usually the right version. If I am uncertain, I note the decision and set a trigger condition — if user count crosses X or latency crosses Y milliseconds, I revisit. The trigger condition is probably the most important part and the part most people skip. Without it, "I will fix it later" becomes "I will never fix it." I write the condition down in the code or in a tracking document so someone — usually me in three months — has to actually evaluate whether it matters.
Get the Full Details

I encountered a specific edge case last year that taught me a useful nuance. I was working on a file processing pipeline where the initial dataset was small enough to fit comfortably in memory. I chose the simple approach: load everything into a list and process sequentially. Six months in, a new client uploaded files that were orders of magnitude larger. The simple approach did not just slow down — it started failing with out-of-memory errors in production. The workaround was not as painful as building the streaming architecture from the start would have been, but it was not trivial either. I had already written integration tests and monitoring around the simple version, which meant the migration path was clear. I switched to a streaming implementation using generators and chunked processing. The code roughly doubled in size, but it took about a day of focused work because the scope was narrow and well-defined. The three weeks I saved upfront by skipping the streaming architecture were mostly erased, but I still came out ahead because for those six months the simple version required zero maintenance. There are also scenarios where this approach completely fails and you should not use it. If the cost of being wrong is catastrophic — data loss, security vulnerabilities, safety issues — you build the right thing the first time. A payment system that loses transactions because you skipped validation is not a deferrable decision. A security module is not a deferrable decision. Medical software is not a deferrable decision. The pattern applies to performance, scaling, and convenience tradeoffs. It does not apply to correctness-critical systems.
Another failure mode is when the simple version and the complex version share almost no code. If you are building something from scratch and the "simple" path requires an entirely different architecture, you are not deferring complexity — you are choosing the wrong path. This happens often with microservice vs. monolith decisions. Writing a monolith and later extracting services is structurally different from writing services from the start. In those cases, the decision is not deferable. I have found that teams using this approach tend to be faster overall, even though individual features occasionally get replaced. The reason is that most of the features you build never need replacement. The overhead of building robust, scalable, abstracted systems for things that stay simple compounds across a whole project. The overhead of occasionally refactoring one thing is much smaller in aggregate. The skill is in the judgment. You need enough experience to recognize which problems are worth solving upfront and which ones are better handled reactively. Junior engineers often skip this entirely and build everything to production grade on day one. Senior engineers sometimes flip the other direction and refuse to build anything properly even when it is clearly needed. Both are wrong. The middle ground is where the approach lives.
If you want to practice this, start small. Pick a feature you are about to build and identify one thing you are tempted to add for future scalability. Question whether it is actually needed now. Remove it. Ship it. Monitor it. If it breaks, fix it. You will learn more from that cycle than from any architectural review board. The pattern is simple enough to remember in a code review. Is this the simplest thing that could work? Is the likely usage profile consistent with that simplicity? Do I have a trigger for when that changes? If the answer to all three is yes, ship it and move on.
