[Originally Posted on LinkedIn]

This article reaches into the wayback machine. I drafted it many months ago in response to to a then-fresh post. Due to other distractions around my then-upcoming book, I forgot about the draft. Yet, the topic is still relevant.

A discussion sparked by Jeffrey Cooper's post on refactoring versus upfront design has highlighted one of software development's most heated debates. The conversation revealed diverse perspectives on how we should approach software design — perspectives that don't necessarily contradict each other but rather illuminate different facets of the same challenge.

My Perspective: Upfront Design is Not Waterfall

I'm all about proper design upfront — but let me be clear about what that means. Upfront design is about attempting to understand and document structures and complex interactions before diving into implementation. It means the team has a shared concept of where they're going, not divergent individual visions. It means having a written-down, clear concept rather than everyone working from their own mental models.

Critically, upfront design does not mean Big Design Up Front (BDUF), nor does it mean waterfall. The idea we now call BDUF has it's roots in the institutional mandates of old (particularly around government systems) that required an all-knowing, inflexible design to be fully completed before the first line of code was written. That was waterfall. There is nothing inherently waterfall about an iterative approach to designing before you implement. In nearly every non-trivial case, thoughtful upfront design reduces overall development time and technical debt.

The iterative approach I advocate means that architecture and interfaces should be understood before component work begins. Each component's structure, interfaces, and behavior should be understood before coding on that component begins. If implementers need to modify the plan, fine. Document the change and feed it back into the design.

Complementary Voices: Evolution Through Learning

Several commenters offered perspectives that complement this approach. Paul Hammond noted that "the ideal setup starts with an initial design and evolves through refactoring over time." This aligns perfectly with my view: start with design, then iterate.

Ray Myers observed that there's no conflict between software beginning to exist and continuing to be maintained. This captures an important idea: design and refactoring aren't opposing forces but sequential phases of the same process.

Jeffrey Cooper himself clarified his position, noting that he believes in "a more robust architecture up front for very complex systems" while acknowledging the value of learning spikes and pathfinders. His hybrid approach—allowing for robust design while enabling rapid iteration—mirrors what I'm advocating.

The Opposition: Perfect is the Enemy of Good

Others in the discussion took a different stance. Allan H. argued that "proper design upfront" implies we already know every step of the problem, suggesting this is unrealistic. William Alexander questioned whether upfront design assumes "you know everything in advance and are always absolutely correct."

These concerns are valid — if we're talking about BDUF. But they mischaracterize thoughtful upfront design. We don't need to know everything to benefit from understanding the architecture. We don't need perfect foresight to gain value from documenting our current understanding and expected interfaces.

The Real Issue: Over-reliance on Either Extreme

Jeffrey Cooper's original concern was about "over-reliance" on refactoring. This is the key insight. The problem isn't refactoring itself—it's treating refactoring as a substitute for thinking through the architecture.

As Jeffrey noted in his comments, when you're building a multitenant healthcare system with PII/PHI at stake, "jumping in with a light design and refactoring along the way is asking for problems." The stakes matter. The complexity matters. The system requirements matter. That thinking resonates with me. I work mainly with high-stakes systems in regulated domains, where getting it wrong has serious consequences, sometimes even life-or-death.

For established teams building well-understood applications, BDUF might work. Sometimes it's just a matter of tweaking a past design. For simple systems, minimal upfront design might suffice. But for new development of complex, high-stakes systems, we need that middle path: clear architectural vision with room for iteration.

Finding the Balance

The software industry has swung from waterfall to agile like a pendulum. What we need is the synthesis:

• Understand your architecture before building

• Document interfaces and expected behaviors

• Create a shared team vision—in writing

• Allow for iteration and learning

• Document changes and feed them back

• Maintain design quality as you evolve

As Jeffrey noted, there's a lot of poor software out there, and most teams claim to be agile. The widespread abuse of "just code it and refactor" suggests we've over-corrected from waterfall's rigidity.

The answer isn't to swing back to waterfall. It's to embrace upfront design within an iterative process. Think before you build. Document what you're thinking. Build iteratively. Learn and adapt. But never lose sight of the architecture.

Because in the end, the goal isn't to be agile or to be waterfall. The goal is to build quality software efficiently. And for complex systems, that requires both upfront thinking and iterative learning.