Want to deliver new product fast? Shortcuts in early phases can bite you in the a$$ later
In agile development, products start simple and grow complex, often leading to a messy codebase that slows progress, but early good decisions can prevent this.
![Seasoned backend software engineer with more than a decade of experience, excels in crafting Java-based applications. He thrives on innovative problem-solving and values the collaborative energy of team projects, driving rapid knowledge exchange. Outside of work, Martin is passionate biker and hiker, and he enjoys capturing the beauty of nature through landscape photography.](https://cdn-web.infobip.com/uploads/2024/07/Milena-1.webp)
In agile development, products often begin small, addressing basic needs, then evolve into complex solutions supporting diverse workflows and features. As the complexity and user base grows, the codebase often becomes messy, slowing down feature additions.
But is this unavoidable? Is there a way to mitigate this?
Let me show you how bad decisions made in the earlier phases can cost you significant resources.
From tiny beginnings to titan transformations
Let’s start with some theory. When beginning a new project or significant feature, you typically start small and iterate. You might create a simple Proof of Concept (POC) to test if a solution addresses the core issue, without prioritizing integration, security, or regulatory concerns.
Once the POC is successful, you build a Minimal Viable Product (MVP), which meets early customers’ basic needs while ensuring stability, security, and compliance. This MVP serves as a foundation for further scalable and maintainable development.
As time passes, the MVP becomes a ‘Polished Product.’ The dev team delivers features rapidly, bugs are fixed, and the customer base grows.
Eventually, scaling beyond the original design becomes necessary, signaling the need for a new version or a new product.
At this stage, scalability and future development are the keys. Understanding the weaknesses of the previous version is crucial. While core logic may be reused if it’s not problematic, the core architecture often needs changes. And nobody has time for that.
Expectations vs reality
At first, teams usually follow the textbook approach, starting with a solid POC using our well-established tech stack. This leads to a simple, database-oriented service for the MVP. That was the case for the team I worked with – but we delivered the first version in record time.
The product and marketing teams excelled, and soon we had paying customers. A true success story!
But let’s fast forward a couple of years to look at that product.
The main part of the service that was there from the beginning is still working, but it’s struggling to handle the amount of work it must do. It is also not easy to add new features or meet new requirements.
Of course, some features were split into microservices, but due to unclear business boundaries in processing, separation wasn’t entirely successful. Many services still interact with the same tables.
Also, advanced analytics require a specialized data source, leading to data synchronization with other services. Changes were often made for specific needs and implemented independently by different teams simultaneously.
As a result, the container diagram became overly complex, and communication between parts of the system was unreliable leading to many bugs.
How can we fix this?
The solution to this problem was slow and costly. We needed to separate various parts of our service properly, make sure they could communicate reliably, and then we could focus on optimizing each service, as necessary.
In other words, we needed to change the whole architecture and rewrite most of our services to accommodate it. Doing so on a running product, while still delivering new features means slow, gradual work, often duplicating functionality to ensure a smooth transition.
Jumping from a proof of concept (POC) straight to production saved us a lot of time and money initially. However, in the long run, the lack of proper architecture has caught up with us.
So where is the fine line between cutting some edges to start earning early and having scalable solutions for years to come?
In my view, the key lies in where you cut corners. Handling non-ideal paths with some leniency is acceptable, ensuring users can retry without data inconsistencies.
However, compromising on architecture, while initially invisible, becomes costly once issues emerge.
Think in boxes
Before diving into code and technology, start with a clear visualization of the problem. Once you understand the product’s purpose, designing a solution becomes simpler. Think in abstract terms first, focusing on basic principles rather than specifics.
This approach may lead to conceptual designs resembling known patterns or technologies. Don’t restrict the solution to just the MVP; consider future requirements too. Addressing issues early is easier than once the code is in place.
Only when you have clarity on how your system should work, you can start thinking about how to implement it. Having some C4 diagrams even before starting real development helps during task breakdown but even more with communication between the dev team and business/product as those should be a common language that both sides speak.
Stay humble – change is the only constant in software development
We’re lucky we are not software pioneers; many problems are already solved.
Libraries and off-the-shelf tech are available for solutions, often cheaper than custom development. While creating new things is exciting, adapting existing solutions is usually more efficient.
When considering technologies for your solution, also consider your existing ecosystem. Consider adopting established technology to accelerate development and ensure reliability, even if it meets most but not all requirements.
When evaluating modern technology, assess its longevity, learning curve, and maintenance needs. Specific requirements may limit options, but choosing a slightly lower-performing alternative that integrates better could be optimal. Introducing new solutions adds configuration and maintenance overhead; improper implementation may perform worse than suboptimal ones.
Today’s needs may evolve rapidly, making future-proofing uncertain. However, anticipating future requirements can prevent your solution from becoming inflexible. If future needs are unclear, consider integrating features used by competitors – they’re likely to become essential eventually.
There is no perfect way to future-proof a solution, so don’t waste too much energy on it. Instead, focus on building reusable components that can adapt to major changes. This applies to everything from microservices to code design within a service.
Don’t sacrifice readability for performance
Nobody wants to write a shi*** code – it is our legacy, our signature that we put out there.
But is it worth it? How much effort would you invest into painting a wall that you know will be torn down in some time?
As I mentioned before, you can be almost sure that the first version of the service will not be there after a few years, unless you follow this anti-example. It’s OK if your code is just good enough, just focus on things that matter.
Well-structured code has clear boundaries which makes it maintainable and reusable. This is in the long-term much more valuable than perfect code with just the right amount of abstraction, precisely crafted naming, or method/class files at just exact size.
It’s surprising how often code sacrifices readability for performance. While performance is crucial for some applications, it’s usually cheaper to add more RAM than to spend hours deciphering complex code. When improving code, consider the overall costs, not just the code itself.
The price of code first – think later approach
Realizing that your foundations are weak while you’re putting up the roof is not just a headache and expensive; fixing it later is almost impossible.
That’s why it’s crucial to take things step by step and regularly check if we’re on the right track. In software engineering, architecture is like the base of a building. If it’s rushed or poorly planned, it’s like building on unstable ground – without a strong base success remains only in dreams.