
What happens when you know what problem you’re solving, but not exactly what to build? You have a broad product concept, an idea of the intended users, and an understanding of the problem the software should solve. What you don’t have is a detailed specification, agreed acceptance criteria, or a reliable definition of what success looks like.
Product Development
Decision Making
TL;DR
- Building without a spec means making irreversible choices with incomplete information. The goal isn’t to guess right the first time — it’s to keep your options open until you know more.
- Configurability isn’t a substitute for requirements. You can make any parameter configurable, but that doesn’t tell you which configuration is correct.
- Validation comes from stakeholder feedback, not from code that runs. A technically perfect implementation of the wrong problem is still the wrong problem.
- Your job isn’t to predict the future. It’s to build systems that can evolve responsibly, make assumptions visible, and give stakeholders a practical way to validate what they actually need.
What You’ll Learn
Design for change
How to structure architecture so requirements can evolve without rewriting everything.
Configurability has limits
Why making everything configurable doesn’t solve the problem of knowing what to configure.
Validate early
How to turn assumptions into testable assumptions before you’ve built too much.
Independent developer tactics
Practical approaches when you’re the architect, the developer, and the product manager all at once.
The Situation You’re In
Building software without a specification is one of the most common situations in software engineering — and one of the most uncomfortable. You have a broad product concept, an idea of the intended users, and an understanding of the problem the software should solve. What you don’t have is a detailed specification, agreed acceptance criteria, or a reliable definition of what success looks like.
You’re not starting from nothing. You know the problem space. You’ve talked to users. You can see where the pain is. What you don’t know is the exact shape of the solution: which fields each screen needs, how approvals should route, what “active” means for a subscription, or how many exceptions the workflow can tolerate before it breaks. You’re standing at the edge of construction with a site plan drawn in pencil.
And yet, you still have to make decisions. Database schemas need columns. APIs need endpoints. State machines need states. Every line of code you write is a small bet about what the final system should look like, and some of those bets are expensive to undo.

Not All Uncertainty Is the Same
It helps to separate uncertainty into two buckets, because you manage them differently.
Requirement uncertainty
You don’t know what the feature should do, who will use it, or how success is measured. This is the dangerous kind — no amount of elegant code can fix it if you’re building the wrong thing.
Technical uncertainty
You know what to build, but not yet how it will perform at scale, how it will integrate with existing systems, or how it will degrade under load. This is solvable — but it needs time, not guesses.
The mistake most teams make is treating both kinds the same way. They spend weeks analyzing requirements they can’t know yet, while rushing headlong into technical choices they haven’t validated. Flip it: spend time understanding the problem, and keep the implementation flexible until the problem proves itself.
Designing for Change
The first instinct when facing uncertainty is often to future-proof: build everything generically, abstract every dependency, parameterise every behaviour, and defer every decision to configuration. The logic is simple — if you don’t know what the requirement will be, make the requirement adjustable at runtime.
This approach has real value, but only when it’s scoped carefully. Instead of assuming every business rule will remain fixed, you separate the underlying engine from the rules and parameters that determine its behavior. Depending on the application, this could mean configurable evaluation criteria, interchangeable strategies, adjustable scoring weights, or a rules engine that can evolve without requiring changes throughout the codebase.
The goal isn’t to build a system that can do anything. It’s to make the changes you actually expect cheaper, without making the things you haven’t thought of more expensive.
Lesson: Design for change, but not for everything
Good abstraction targets the things that actually change in your domain — pricing rules, notification channels, role permissions — not the mechanics of HTTP requests or database connections that never change. Abstracting the stable parts of your system just adds indirection without flexibility.
Configurability Isn’t a Substitute for Requirements
There is an important limitation, however. A configurable system can accommodate different requirements, but it cannot determine which requirements are correct.
Without agreed acceptance criteria, an engineer can build a technically sound implementation that still fails to meet the stakeholder’s expectations. The system may work exactly as designed while the design itself remains an unverified interpretation of the original problem.
Consider a reporting dashboard you build with configurable chart types, export formats, and date ranges. It’s elegant, extensible, and architecturally sound. But if the stakeholder actually needed daily anomaly detection with Slack alerts, your dashboard solves nothing. You built the infrastructure for answers they never asked.
This distinction matters more than it sounds. Software quality is not just about whether the code works. It is also about whether the system solves the intended problem, satisfies the agreed requirements, and behaves correctly under realistic conditions. A system that perfectly implements the wrong specification is not high-quality software — it’s expensive technical debt wearing a clean architecture.
Lesson: Configurability shifts the decision, it doesn’t remove it
Making a setting configurable moves the decision from “now” to “later”, but someone still has to make it. If you don’t know the answer today, making it a dropdown doesn’t create the answer — it just defers the argument until someone has to pick a default.

The Importance of Validation
A more reliable development process combines flexible architecture with early stakeholder feedback. Before substantial implementation begins, establish:
- The problem the system must solve. Not the solution — the problem, in the user’s words.
- The expected inputs and outputs. What goes in, what comes out, and what “correct” looks like.
- The business rules and evaluation criteria. How do you know if a result is right? Who decides?
- Measurable acceptance criteria. Not “it works,” but “it works when X, Y, and Z conditions are met.”
- A process for reviewing results and resolving disagreements. Who breaks ties when the product manager and the lead engineer disagree?
Where some details cannot yet be finalised, document the assumptions explicitly and identify which decisions remain outstanding. An assumption document is not a replacement for requirements — but it turns invisible guesses into explicit risks that can be tested, revisited, and retired as you learn more.
A prototype should then be treated as a hypothesis to validate, not proof that the final product requirements have been satisfied. If you build a prototype and stakeholders love it, that’s evidence — not proof. The real validation happens when someone tries to use the system for its intended purpose and finds the gaps you didn’t know were there.
Not all validation costs the same. Spend the cheapest test that can kill the riskiest assumption:
| Technique | Cost | What it proves |
|---|---|---|
| Five-user conversation | An afternoon | Whether the problem hurts enough to pay for |
| Clickable mock | A day or two | Whether the workflow matches reality |
| Landing page + signup | A weekend | Whether anyone wants it at all |
| Wizard-of-Oz pilot | One to two weeks | Whether the full loop works end to end |
| Paid trial cohort | Several weeks | Whether it survives contact with production |
Lessons for Independent Software Developers
For independent developers and small engineering teams, uncertainty can be especially challenging. You may need to make architectural decisions before every business question has been answered. You may be the architect, the developer, the product manager, and the customer support — all at once. That means every architectural mistake is yours to live with, and there’s no dedicated requirements analyst to blame when things go wrong.
Good engineering can reduce the cost of uncertainty, but it cannot eliminate the need for collaboration. You still need someone — a client, a user, a stakeholder — to look at what you’ve built and tell you whether it solves their problem.
Three tactics help when you wear every hat. First, timebox the architect role: give yourself one day to make the big calls, write them down with reasons, and then switch hats — endless deliberation without new information is procrastination in a trench coat. Second, buy feedback cheaply: a clickable mock shown to two users beats a month of solo refinement, and a landing page with a signup form is a valid experiment. Third, keep a decision log alongside the assumption log — not just what you believe, but what you chose and why, so future you can distinguish “we knew this was risky” from “we never thought about it”.
The objective is not to predict every future requirement correctly. It is to build systems that can evolve responsibly, make assumptions visible, and give stakeholders a practical way to validate what they actually need.
Lesson: Solo builders need process most
Teams get feedback for free through disagreement. Solo developers get silence — which feels like agreement but isn’t. Compensate deliberately: schedule demos with real users, write decisions down, and treat your own confidence as the least reliable signal in the room.
A Practical Checklist for Building Under Uncertainty
When you’re staring at a blank editor and a vague problem statement, use this as a starting point:
Write the Problem Down in One Paragraph
If you can’t articulate the problem in plain language, you don’t understand it well enough to build for it. Not an elevator pitch — a paragraph that defines who has the pain, what they’re trying to do, and why current approaches fail them.
This paragraph becomes your compass for every later decision. When a stakeholder asks for a new field, a new role, or a new workflow, you hold it up against the paragraph: does this serve the problem as written, or has the problem itself shifted? If the paragraph no longer describes reality, update the paragraph first — then the code. Teams that skip this step end up with a codebase full of features nobody asked for, each one added because someone important mentioned it in passing.
Lesson: Vague problems produce vague software
If your problem statement contains words like “streamline”, “leverage”, or “enhance” without naming who benefits and how, rewrite it. Concrete nouns — names, numbers, workflows — are the difference between a paragraph that guides decisions and one that decorates a pitch deck.
List Your Assumptions Explicitly
“The user will have an admin panel,” “Tenant data will never be shared,” “Volume will be under 1,000 records.” Write them down so they can be tested and retired. Three assumptions that seem obvious but each change everything if they’re wrong.
Give every assumption an identifier, an owner, and a status — untested, testing, or retired. The identifier matters more than it sounds: when assumption A-02 turns out wrong six weeks later, you want to find every decision that depended on it in seconds, not by re-reading the codebase. Review the log weekly. Retire assumptions the moment evidence arrives, and treat a retired assumption as a trigger to re-examine everything built on top of it.

Identify Irreversible Decisions
Database schema, third-party integrations, authentication models — these are expensive to change. Everything else can wait. Don’t spend three weeks choosing a message queue before you know whether users even want the feature.
A useful test: ask what it costs to reverse the decision in six months. Renaming a button costs minutes. Migrating a live database to a new tenancy model costs weeks and risks every customer at once. Spend your certainty budget on the expensive reversals; let the cheap ones stay fluid until evidence arrives. The table below is a starting point — fill in your own rows for your own domain.
| Decision | Example | Cost to reverse later |
|---|---|---|
| Data model & tenancy | Single shared schema vs schema-per-tenant | Very high — live migration under load |
| Identity & auth | Auth0 vs self-hosted Keycloak vs passkeys | High — every user must migrate |
| External vendors | Stripe vs Payfast vs custom billing | Medium — contracts, webhooks, reconciliation |
| UI copy & layout | Onboarding flow wording, dashboard cards | Low — deploy and measure |
| Feature flags | Which experiments ship to whom | Trivial — flip and observe |
Build the Smallest Thing That Tests Your Riskiest Assumption
If you’re unsure whether users will pay for the feature, build the payment flow before you build the feature itself. If you’re unsure whether customers will use a mobile app, build the mobile web page first.
The discipline here is sequencing, not speed. List your assumptions in order of how much damage each one does if wrong, then attack the list from the top. Most teams do the opposite: they build what’s easy or fun first and discover the fatal assumption months later, when unwinding it means throwing away the easy work too. A week spent validating the riskiest belief beats a month building on top of it.
Lesson: Easy first is expensive later
Greenfield setup, CI pipelines, and boilerplate feel productive precisely because they avoid the hard question. If a week passes without testing a real assumption, you’re not de-risking — you’re decorating.

Set Review Points
Decide in advance when you’ll stop and re-evaluate — after two weeks, after the first customer trial, after the MVP launches.
Pre-commitment matters because sunk cost whispers. Without a scheduled gate, every review becomes “we’ve come so far, let’s keep going” — even when the evidence says stop. Write the gate criteria down before you start: what must be true at each checkpoint, and what happens if it isn’t. A gate without kill criteria is just a meeting.

Document What You Don’t Know
An “unknowns” document is not a failure. It’s a list of things you intend to learn, and it prevents assumptions from masquerading as facts.
Keep it short and keep it current: each unknown gets one line for what you need to learn, one line for how you’ll learn it, and one line for when. Review it at every gate. An unknowns list that never shrinks is telling you something — either you’re not testing, or the problem is bigger than the current plan admits.
When Flexibility Becomes Its Own Risk
There is a failure mode on the far side of “design for change”: a system so configurable, so abstracted, so determined never to be wrong that nobody — including its authors — can say what it does. Every behaviour hides behind three layers of indirection. Every simple change requires updating a rule engine, a feature flag service, and a plugin registry. The codebase doesn’t encode decisions anymore; it encodes the fear of making them.
Watch for the symptoms. If onboarding a new developer takes weeks because “you just need to understand the framework first”, the framework is the problem. If debugging requires tracing through five strategy objects to find the one line that matters, the abstraction has inverted: the simple case now pays the complexity tax of the general case. If your configuration files are longer than the code they configure, you haven’t built flexibility — you’ve built a worse programming language on top of the one you had.
The antidote is boring code in the stable places. Write the straightforward implementation for what you know today, isolate the two or three things genuinely likely to change behind narrow interfaces, and leave everything else concrete. Future you — armed with actual requirements — is far better equipped to abstract correctly than present you, guessing in the dark.
Lesson: Flexibility has a carrying cost
Every abstraction charges rent: cognitive load on every reader, indirection on every debug session, surface area on every change. Only pay it where volatility justifies it. Everywhere else, boring code wins.
A War Story: The Configurable Reporting Trap
A few years ago I built exactly the dashboard described above: configurable chart types, export formats, date ranges, role-based visibility, scheduled delivery. Six weeks of careful engineering. The architecture review praised its extensibility. The demo went well — stakeholders nodded at every dropdown.
Then came the first real request: “Can it flag it?” support tickets spiking on weekends, emailed to the on-call engineer before Monday standup. Nothing in the configurable dashboard could do that. It could render any chart of historical data, but it couldn’t watch data, decide anything, or notify anyone — because nobody had asked for anomaly detection, so nobody had designed for it. The configuration space covered every variation of the wrong thing.
We bolted on a separate alerting service in two weeks. It was ugly, hardcoded, and exactly right. The lesson stuck harder than any architecture book: the configurable system answered every question except the one that mattered, because configurability explores the space you imagined — never the space you didn’t.
Lesson: Ask what decision the software enables
Before building anything configurable, ask: what decision will this help someone make, and what happens if they decide wrong? If you can’t answer, you don’t need configuration — you need a conversation with whoever will live with the consequences.
Conclusion
Building software without a specification is not ideal, but it is common — and it is survivable. The goal is not to eliminate uncertainty. You can’t. The goal is to keep uncertainty from becoming expensive.
Design for change when requirements are uncertain, but never confuse technical flexibility with a substitute for clear requirements, measurable acceptance criteria, and ongoing feedback. Build the smallest thing that tests your riskiest assumption, document what you don’t know, and treat every line of code as a hypothesis until someone uses it and tells you otherwise.
If you’re building something where the requirements still feel fuzzy, start with the problem, not the solution. Everything else follows from there.
And keep this post’s checklist somewhere visible — on the wall, in the repo readme, at the top of the project board. Uncertainty doesn’t announce itself; it accumulates quietly in the gap between what you assume and what you’ve verified. The teams that survive it aren’t the ones that guess best. They’re the ones that check most often.
Building Software Without a Clear Spec?
We help South African businesses design and build software when the requirements are still taking shape — architecture that doesn’t lock you in, and development that validates assumptions before they become expensive mistakes.