There is no final version
How to manage evolving software products without wasting effort

If software engineering were like traditional house construction, we would be constantly moving walls, replacing plumbing and rebuilding the second floor – all while the homeowners are moving in.
In an agile ecosystem, code, architecture, and UI are in constant motion. Software is continuously evolving as the product team works toward its ambitious long-term goals in small, focused, end-to-end iterations.
However, no plan is final. Throughout the development process, requirements are often added, removed, updated, reverted, and deleted again based on internal and external user feedback, technical learnings, changed business priorities, reshuffled roadmaps, platform dependencies, and more.
Let’s look at how we can navigate this continuous evolution in practice, while minimizing waste and maximizing delivered customer value. We will follow the life of BambooBoost, the fictional panda health application introduced in Manageable Iterations.
I. Working in iterations
In the previous article, we saw how a product vision can be transformed into small, incremental changes delivering end-to-end value. But working in short iterations has an obvious consequence: the UI, the business logic, and system architecture are continuously evolving.
Let’s take two examples from BambooBoost.
Evolution of the panda list
The user story breakdown example from the Manageable Iterations article showed how a bigger feature can fit into a series of self-contained user stories, adding more and more UI elements each sprint until the desired end state (full-fledged panda list with search and filter features) is reached.
- As a user, I want to see a single panda with limited information (name, age, location).
- As a user, I want to see a list of pandas with limited information.
- As a user, I want to see the wellbeing score on the list of pandas.
- As a user, I want the panda list to be sortable and pageable.
- As a user, I want to filter the list of pandas by criteria X, Y, Z …

Building user interface functionality incrementally means the frontend is never final. The team should be ready to continuously adapt the functionality of their UI and backend APIs while the initial simple version is growing to a production-ready panda management interface.
Evolution of the wellbeing score
Another example of iterative changes is the wellbeing score logic, concerning the business domain layer.
The panda wellbeing score is a real-time representation of the tracked animal’s health on a scale of 0 – 100, based on various sensor data and other factors like food intake.
The final complex formula is not implemented upfront. The calculation goes through quite an evolution during the development phases of BambooBoost:
- For demoing the product idea, a simple hardcoded value would suffice.
- While working on the prototype processing movement sensor data, we can simply make the score binary, e.g. 0 if no movement, 100 if at least some movement is detected, to demonstrate the successful integration of the sensor’s data processing pipeline end-to-end.
- Later, we introduce an abstraction over the various supported data sources and delegate the calculation to the appropriate strategy class. The calculation logic can be easily adapted again once a new source of data is available that influences the final score.

II. Learn and adapt
As we progress with the development cycles and validate our ideas, we gradually learn what works and what does not. If necessary, we need to adapt our solution on the go, since we want to deliver a product that is technically solid and effectively solves the core problems of our customers. Naturally, this leads to even more changes. Let’s see a few examples.
UI restructuring
Let’s imagine that we came up with a design for the panda vitals overview listing all important metrics that users might need, based on some initial market research and high-level customer interviews.
Once the prototype version is done and we can do some more user testing, we receive some rather negative feedback about this dashboard. Customers are finding it overwhelming to see that many charts, especially on mobile. Also, they would prefer something more actionable, they want to know if their pandas need immediate attention or if they are all fine.
Based on this feedback, the team could redesign the layout to highlight the wellbeing score and allow the user to focus on metrics that need immediate action:

During the implementation phase the team could delete unnecessary UI components and API calls, replacing the original layout with the simplified view that gives a quick overview with actionable insights in a mobile-friendly way.
Re-architecting
Besides user feedback-driven learnings, the team can also discover a lot about the technical side of the solution during the development and validation phases.
We often lack all the data needed to make long-term architectural decisions at the very beginning of the development. We start building the product with some initial assumptions, then we may need major rework or structural change during development if some of those assumptions turn out to be incorrect. It can also happen that we need to change the foundations of the software due to external factors (licensing costs, compliance, platform dependencies, etc.)
Examples
- We learned that the original stream-based architecture was not suited for the panda wellbeing analysis feature, as users wanted to run complex, long-term analytical queries that Kafka isn’t built to handle. As a solution, we routed historical data from Kafka to Snowflake for long-term trend analysis, while keeping Kafka as the event streaming backbone for real-time preprocessing and wellbeing score calculations.

- We learned that some of our initial assumptions about the data model were wrong. Data coming from the various sensor types turned out to be highly polymorphic, as different sensor types required vastly different payloads to be stored and queried, making it a suboptimal fit for our relational database. For a better-fitting model, we dropped the ORM for the sensor readings in favor of storing dynamic data in JSON documents instead.
- We learned that a particular 3rd-party data ingest client library does not offer proper error handling and retry mechanisms, so we opted for implementing our own client to ingest data from the external system reliably, throwing out the initial logic using the unreliable 3rd-party version.
Learning is not failure
The overall impact of these re-architecting efforts on the codebase can range from medium to very significant, depending on the scope of the change and how early in the development process the problem is discovered. While they involve a real cost, they should be viewed as empirical learning rather than failure to “get it right” the first time. Fortunately, the impact can be softened by the practices discussed in the next section.
III. Fighting waste with Lean
If software is never done but evolving constantly, both for planned and unplanned reasons, what can we do to minimize the pain that comes with handling all those changes? How can we avoid throwing out huge chunks of finished work and detect early if a UI redesign or tech stack change is required to stay on course and deliver a working product?
Some answers may be found in the practice of Lean Software Development. This methodology applies manufacturing principles in the software world, aiming for maximizing customer value and minimizing waste.

What is considered inefficiency in software engineering ranges from unnecessary features, low-quality code causing bugs to multitasking overhead, but first and foremost it is unfinished work (“unsold inventory”).
Working on a product or feature for months only in a dev environment without a single release is considered a major waste. It produces a huge pile of code full of unproven assumptions and risks both from the technical and the product side. Will the code work in production? Will it scale? Do users really need this product? Do they want to use it? Can they figure out how to use it?
By following a few key guidelines based on the Lean principles, we can make the development process far less wasteful, helping the team to deliver valuable software faster and minimizing the risk of working on unproven software month after month. This list is not exhaustive, however, each of these items can be rather impactful when it comes to dealing with changes and uncertainties:
- Deliver smaller iterations faster
- Defer commitment
- Right-size quality
Let’s look at those selected guidelines in more detail.
1.) Deliver smaller iterations faster
What?
Aim for the smallest possible releasable iterations, both for high-level product milestones and for low-level execution planning. Introduce repeated short cycles with validation, CI/CD with continuous delivery to (internal) product. Hold stakeholder demos, gather feedback from potential users, internal users, early adopters and so on. Evaluate product and technical learnings for each cycle and adapt the plans for the next iterations if necessary.
Why?
Minimizes the risk of producing waste by:
- validating the technical solution in production (functionality, configuration, access control, performance, etc.)
- validating the product itself (usability, customer value, potential for product market fit)
This is the most straightforward piece of wisdom. Delivering smaller iterations faster and learning from feedback means that we can validate our assumptions with potential users and verify our software under real production conditions. Smaller iterations also allow us to react to changes and shift priorities faster.
Software does not need to be perfect or done for the first release. But we will already have vital signals about customer satisfaction and data to decide on the future direction.
Smaller Iterations – Example
For an example, refer back to the previous article. The section “Thinking in Evolving End-to-End Use Cases” illustrates how the panda movement tracking and the initial version of the panda management system can be broken down into smaller, E2E testable iterations to deliver software faster for technical validation and user feedback.

- Iteration 1 (Walking Skeleton): A bare minimal E2E version that performs a small, meaningful slice of core functionality. Focus on connecting components. For example: one hardcoded business entity (“Demo Panda 123”), process a single dummy sensor reading, overly simplified wellbeing score calculation (100 if panda moves, 0 if not). Establishes the production-ready structure (architecture, CI/CD pipelines, observability) and “connects the dots”.
- Iteration 2 (MVP Single Zoo): Processes sensor data of multiple pandas from the same location. Basic CRUD functionality in the management interface. Wellbeing tracking based on simple daily/hourly movement calculation
- Iteration 3 and Beyond: Support multiple zoos, allow users to configure sensor data sources, iterate on the UI/UX design, scale-up sensor and error-proof data processing (circuit breakers, multi-AZ deployment), and finally, add polish and “nice-to-have” features (search, reporting) for the public release.
2.) Defer commitment
What?
Avoid early commitments to a fixed architecture, huge upfront design, “final” abstractions and data models, planning for all possible use-cases, error scenarios, performance and reliability problems. Keep things simple and open as long as possible, limited to the requirements of the current iteration.
Why?
For software products, it is practically impossible to come up with the “perfect” solution covering all future requirements on the first try. It pays off just to put together something simple based on initial assumptions, even best guesses, and defer “point of no return” decisions to a later stage when more data is available. A less polished initial implementation reduces waste by keeping options open for future architectural decisions.
Implementation decisions and product requirements are often re-evaluated as the team progresses with the development and release cycles and gains more insights about the solution. Code will be changed, maybe even deleted completely. By focusing on delivering the easiest possible solution for the initial requirements, we can move faster, the code will be simpler, easier to change and less wasteful to throw out later.
We must also refrain from making a lot of assumptions about future needs, adding extra frameworks or over-engineering solutions for “future possibilities” that may never be prioritized for actual implementation. Focus on what we need now and gradually build on it instead (YAGNI).

Evolutionary Architecture
One technique that follows the “Defer commitment” philosophy is the Evolutionary Architecture, advocating to kick off development with no/minimal upfront architectural design.
We can start by building a prototype for the core idea of the system using hardcoded and oversimplified implementations for most parts, while still keeping it at acceptable quality, validated by architectural fitness functions (automated tests that measure characteristics like performance, security, and coupling).
While transitioning to GA, we need to keep growing and replacing the prototype’s quickly built modules with real infrastructure, frameworks, system design patterns and implement “hardened” components designed for high availability and scale.
Crucially, this evolution is based on the continuous feedback we get from the released iterations, evaluating how the system performs under real conditions and gathering insights from early adopters to ensure that we are building the software that customers actually need.

Defer commitment – Examples
Data model and storage technology
Designing a polished BambooBoost SQL database with 20 entities and 100 relations, transactions handling, sharding, multi-AZ deployment on day 1 and supporting it for the whole development process is a huge effort. If this solution needs to be thrown out or significantly changed due to requirement changes or incorrect assumptions in the design, it will be a major waste.
A better alternative could be to put together something simpler for the MVP. It could be a gradually built, minimal schema with one DB instance running, no read/write optimizations applied yet.
We could also defer the data storage decision altogether. We can start with technology-agnostic interfaces for persistence and opt for a minimal-effort, serverless JSON document store implementation until the data model stabilizes and we understand what kind of query patterns we could expect from the first real users. At that point, we can make an informed decision on the suitable storage solution, schema, indices and other optimizations for the publicly released version.
Abstraction for notification delivery channels
Introducing several layers of abstractions for the panda email notification logic may be overkill for the first iteration. We might add more delivery channels later, in which case a cleverly designed class hierarchy could help to simplify the client code, however, if we only need email sending capabilities and nothing else for the first three years of BambooBoost, the prematurely introduced abstractions will only add cognitive overhead while working with the code.

3.) Right-size quality
What?
Keep quality at the optimum throughout the software development lifecycle, raising the bar gradually while progressing from a feasibility prototype to a polished business-critical product. The software delivered at the end of each iteration should be clean and free of critical issues (internal quality), however, non-functional requirements and external quality measures like reliability, scalability, UX polish and edge-case coverage must be defined carefully based on the target maturity level (prototype, development, internal or public beta, general availability).
Why?
Right-sizing quality means that the team can focus on what matters for the current maturity level and complete the milestones accordingly. Users can start using the WIP product earlier, providing crucial feedback faster if we avoid overinvesting effort into topics like making a prototype UI look like a final product or scale up an early dev version to 100k parallel queries and 99.999% availability.
Avoiding the overinvestment trap before technical validation and user acceptance testing also minimizes the waste caused by potential UI restructuring, re-architecting efforts initiated later.
Aligning on quality
Since there is no “one size fits all” solution and contributors may have different quality targets in mind, the product team needs to align on the sensible level of effort to put in for each development milestone (maturity level). Generally, we should avoid the extremes and favor the sweet spot between the “move fast and break things” (garbage) and “we need perfect solutions” (gold plating) guidelines:
- No garbage. Software is always “work in progress”, however, a single iteration must be finished completely. It should be clean, functional, free of critical issues and vulnerabilities. Implemented interface elements (UI or API) must be working properly, allowing the stakeholders and early adopters to validate the results in a production-like environment. Important non-functional requirements like security, observability, and configurability should not be neglected in favor of moving faster in the short term.
- No gold plating. Most of the remaining non-functional requirements like reliability, scalability, data integrity and the definition of “acceptable quality” should heavily depend on the product maturity level. In alignment with the Lean philosophy, overinvesting effort into a non-validated product idea, perfecting, polishing prematurely should be avoided.
Example Quality Targets for different Product Maturity levels
| Maturity level | Target Audience | Primary Focus |
|---|---|---|
| Prototype | Internal Stakeholders | Feasibility |
| Internal Development | Product Team | Feature Development, Early Feedback |
| Internal or Public Beta | Internal Adopters, Early Adopters | Technical and Product Validation |
| General Availability | Entire User Base | Scale, Reliability |
- Prototype: Critical “Happy path” works, mock data and oversimplification is acceptable. Not in scope: polished features, configurability.
- Internal Development: Features are working and demoable. Not in scope: scalability, availability SLA, backward-compatible schema and API changes. The system can break and test data may be lost.
- Beta: Adoption can be started in non-critical business cases. Supports a limited amount of active users, reasonable uptime. Observability is critical for collecting data. Not in scope: high availability, completely reliable and bug-free.
- General Availability: Polished UX, high availability, full observability, operational supportability.
“Not in focus” does not mean “ignore”
Right-sizing quality does not mean ignoring foundational principles or writing spaghetti code. Certain non-functional traits like scalability, fault tolerance, access control cannot simply be “added later” to a monolithic, tightly coupled system full of tech debt between the Beta and the GA versions.
Instead, right-sizing quality means designing for extensibility. In early iterations, we build simple implementations, but enforcing clean architecture with clean domain boundaries, decoupled implementations, stateless execution, and so on. This ensures that when scaling up requires a distributed implementation, we can upgrade the existing components for distributed infrastructure without tearing the core business logic apart.
Fighting waste – TL;DR
✅ Start with something simple → validate → improve and grow the software repeatedly
✅ Focus on the current requirements and product maturity level
⛔ Don’t try to fit months of work into a single release
⛔ Don’t try to decide on the perfect architecture solving all future problems
⛔ Don’t build perfect software before validating customer value
IV. Supporting practices
As an addition to the highlighted Lean principles, applying industry best practices can also help to stay productive with fast-paced changes, allowing the codebase to evolve without turning into a big ball of mud.
Refactoring
The iterative approach requires continuous refactoring to keep the codebase clean while the team implements complex logic to replace the initially introduced simplified components. New features require the introduction of new abstractions, generalization, splitting or merging modules, creating reusable components, and completely changing data types for the whole call hierarchy.
Guidance: Refactor with purpose and with concrete scope in mind. Examples of good reasons to refactor in BambooBoost include data type changes to prepare for supporting multiple sensor sources in wellbeing score calculation logic, or before introducing new, complex panda filtering which the system was not prepared for.

Test Automation
Having proper test automation in place gives the team confidence that the changes they introduce do not cause regression issues. Automated testing is a critical safety net for refactoring.
Guidance: The level of testing to be supported also changes with product maturity level. Full UI test coverage and scale-out performance testing for an early dev version might be a wasteful activity, however, an appropriate level of test coverage, especially unit and integration tests as well as happy path smoke testing is a must at all maturity levels.
Continuous Delivery with Feature Flags
Pushing out everything as soon as possible for technical validation and internal testing is a core element of agile development. Ideally, all changes should be deployed to the dev environment instantaneously and released to hardening/internal production at the end of each iteration, carefully hidden from the general public with feature flags, but allowing stakeholders and early adopters to test continuously as the system matures.
Guidance: While releasing WIP features, risks of negative impact on other parts of the system should be addressed, including: hardening security against cyber attacks, setting up observability metrics to monitor performance and usage, introducing feature flags to control rollout to a selected user base and kill switches to terminate if something goes horribly wrong.
Observability & Analytics
Observability data helps with tracking the behavior of the components in production, on realistic infrastructure setup and under realistic load. It is also critical to track how the internal users and early adopters interact with the software, which features are used, what volume of data, what kind of queries and usage patterns we see coming from them, so we can gather meaningful insights and adapt our plans for future iterations.
Conclusion
There is no final version. Agile software development is all about small increments and continuous learning that bring us closer to a product that solves constantly changing customer needs.
Software is never finished. However, by embracing Lean principles – delivering small vertical slices, deferring heavy commitments, and right-sizing quality at each maturity level – we can eliminate waste without compromising technical integrity. Supported by conscious refactoring, evolving architecture, and strong observability, managing constant change becomes a competitive advantage in delivering a great product.