Solve Problems

Problem-First Programming

Introduction

A programmer’s job is to solve problems. In practice, this rarely looks like implementing a clean solution from start to finish. Most non-trivial problems require refactoring, partial rewrites, or even a change in direction altogether as new information is uncovered.

A common instinct is to try to minimize rewrites by aiming directly for a “final” solution. While this is well-intentioned, I’ve found that this approach often has the opposite effect: it increases complexity early, slows iteration, and makes it easier to solve the wrong problem well. When understanding is incomplete—which is almost always the case at the beginning—designing as if it were complete leads to brittle solutions.

For that reason, I believe productive programming requires deliberately separating exploration from optimization. Early progress should prioritize understanding the problem space rather than prematurely converging on a polished result.

Quick and Dirty Is Often Correct

One way to avoid wasted effort early is to write only the code that is immediately necessary. Speculating about future needs tends to introduce unnecessary generality and complexity before the problem has earned it.

A concrete example comes from a game project where I implemented a general free list. Anticipating frequent reuse, I designed it to be flexible and broadly applicable. In practice, I ended up using far fewer instances than expected—and free lists themselves are a very simple data structure. Writing a general solution and integrating it took significantly more time than writing a few targeted implementations would have.

This pattern appears often: generality feels safe, but it carries a real cost. Until usage patterns are clear, general systems lack the context needed to be designed well.

Code quality certainly matters for long-term maintainability, but polishing code that is likely to be deleted or restructured provides little value. Perfect variable names, formatting, or exhaustive comments are rarely the bottleneck early on. Worse, detailed comments written during exploration often become stale and misleading, increasing cognitive load rather than reducing it.

Early code should be disposable. Its primary job is to answer questions, not to survive indefinitely.

Exposing Complexity Early

Large problems naturally decompose into smaller subproblems, but not all subproblems deserve equal attention at the start. Fully committing to the hardest component early can be risky: a later discovery may invalidate the approach entirely.

Instead, it is often more efficient to work on nearby or simpler components that expose the real constraints of the system. This can surface hidden complexity without over-investing in a fragile solution. Basic bug fixing and lightweight documentation can be valuable here—not to finalize a design, but to make the difficult parts visible.

One example from my game involves the asset management system. Under rare conditions, an asset could be unloaded between the time it was requested and the time it was used, resulting in invalid data. Fully solving this problem would likely require careful synchronization across multiple threads.

However, the occurrence was rare and the consequences were limited. By documenting the behavior and understanding when it could occur, I was able to ensure it wouldn’t impact the final game if the system persisted—while still making progress on more pressing problems. Not every known issue needs an immediate, comprehensive solution.

The key is recognizing which complexity must be addressed now, and which can be safely deferred.

Compression After Understanding

Once a solution is stable and the problem is well understood, patterns begin to emerge naturally. This is the right time to compress code: collapsing common logic, introducing abstractions, and shaping general systems around proven needs.

A compression-oriented mindset helps prevent wasted work. Instead of forcing the problem into a preconceived abstraction, abstractions are allowed to form around the problem. This usually results in simpler interfaces and fewer special cases.

Delaying abstraction also keeps reasoning local. By staying close to the concrete problem, it becomes easier to understand performance characteristics, failure modes, and invariants. Once those are clear, priorities can shift toward maintainability and optimization.

There is, of course, a balance. Extremely poor structure or obvious algorithmic inefficiencies can hinder progress. The goal is not to avoid cleanup entirely, but to ensure that cleanup is informed by real usage rather than speculation.

Conclusion

Solving difficult problems requires continuous judgment about where effort is best spent. Focusing too early on polish or generality often obscures the real challenges, while deliberate exploration builds understanding and momentum.

Being mindful of how time is allocated—what is exploratory, what is provisional, and what is intended to last—helps ensure steady progress. For me, staying focused on the immediate problem at hand leads to clearer thinking, faster iteration, and better results overall.

Reducing speculation and embracing temporary solutions may feel uncomfortable, but it is often the most direct path to solving the right problem well.

Previous
Previous

Ideas About a New Low-Level Visual Programming Language

Next
Next

Life Without Recursion