Architectural Decisions and Incremental Product Development
Discover strategies to avoid unmaintainable, fragile systems by making strategic architectural decisions in agile development.
Agile development teams continuously produce small product increments. Each increment delivers additional value to users, often associated with new or improved functional capabilities (features).
That approach leads to local optimizations. You build and release features over a longer period of time, always focusing on the next increment. After a while, you might find that your system grew and became unmaintainable, fragile, and unreliable. To some degree, we need strategic decisions, principles, and rules to keep this in check. However, too much of this can result in over-engineering and rigidity.
Let’s discuss how to strike a balance between short-sightedness and over-engineering.
Product Increments and Quality Attributes
We can’t reason about a successful software product by only focusing on functionality and features. Each system must uphold certain properties like reliability, performance, or security. We call those properties quality attributes. You can only fulfill quality attributes by following foundational concepts or principles, often cutting across many parts of your system. Need to support millions of users simultaneously? You could rely on established and scalable infrastructure from a public cloud vendor. Need to provide calculation results within a few milliseconds? Maybe you front-load expensive calculations by caching intermediate results across all calculation modules. Need to make sure that specific functionality is easily modifiable by developers? Your team could decide to design a bounded context around the functionality and extract it into its module.
When you develop a particular product increment, you plan on delivering value to your users. So you need to deal with some quality attributes immediately, while others won’t play a role in the upcoming increment. For example, if you plan to deliver new calculation modules, you may want to design a caching solution to reach your performance goals. But, if your product does not yet need to support millions of users, you could delay decisions about this quality attribute.
Product Increments and Strategic Decisions
Your first consideration should always be: “What do we need to consider in the upcoming increments to fulfill the quality attributes we aim for?” If this is the first iteration where you need one service to talk to another, you must at least decide which communication mechanism you want to use.
You could delay other considerations, e.g., how to communicate with that third-party API if the corresponding user story is four iterations down the road. You only have to make sure that your current architecture does not impede that implementation when it comes up. That's where having an architectural vision (a rough idea of the target architecture you are currently aiming for) helps reduce risks and mitigate issues that might come up down the road.
After identifying a highly relevant decision for your next product increment, you want to identify potential options to solve that question. Having many options is a good thing because it increases your decision flexibility.
Options and How to Deal With Them
Your knowledge of a particular option may affect your confidence in building an increment that meets certain quality attributes. Choosing an option you are less optimistic about is okay if you can easily migrate to another option later on.
Figure 1 shows that. The vertical dimension shows your confidence in the option, while the horizontal dimension depicts the cost of migrating away from it to another option. Each field in the quadrant shows how to deal with a given option.
Just do it
Example: A team chooses a familiar message bus and knows it achieves the required throughput, which is critical for achieving performance goals. The message bus also supports an open standard (Java Message Service). That makes it interchangeable with other messaging solutions if the throughput nevertheless turns out to be insufficient.
Options where you have high confidence their implementation will achieve your quality attributes with low migration costs are ideal. They have shallow risks, and you should implement them directly.
Evaluate fast
Example: A team must choose a relational database. First, it will find out which DBMS best supports its load-heavy query patterns. It decides to hide the database behind a Java Persistence API abstraction and tires out different database management systems in production.
When you are unsure about the outcome of implementing the option but it is easily exchangeable, you should evaluate it as fast as possible. You can even assess multiple options, e.g., by trying one solution and switching it out for another. You could also release one solution to half of your users while releasing another solution to the other (A/B testing).
Evaluate and close migration options
Example: All development teams in the company are familiar with Kotlin. Kotlin is known to be appropriate for building readable code and supporting maintainability. A newly formed team responsible for implementing mathematical models decides on the programming language they want to use. Most team members favor Kotlin. But some worry about using a JVM-based language for complex mathematical models, fearing it may hurt performance. So, they decided to build a Proof of Concept in Kotlin, validating whether performance criteria are met. After it turned out to be the case, they did not evaluate other options further and continued working with Kotlin.
With high confidence in an option but high migration costs, you should figure out the riskiest parts of implementing the option and evaluate them quickly. Afterward, don’t actively keep migration paths open; go with the chosen option. (Note: that holds if requirements for quality attributes are believed to stay stable. If they change and evolve, it might be reasonable to keep migration paths open nevertheless).
Evaluate and keep migration options open
Example: Two development teams want to split functionality between their microservices. They came up with an idea of which functionality should belong to which service but remain skeptical about whether this split is a good idea. They decide to implement the solution but ensure future refactorings remain possible. So, they aim for a consistent tech stack to ease moving code between the services (or merging the services into one).
In this case, switching is both challenging and likely. The way to go here is to think strategically about migration paths and actively keep them open until the confidence in the solution rises.
Conclusion
Strategically handling decisions and their options is essential in agile product development. Address necessary questions for upcoming iterations. At the same time, actively keep necessary migration paths open and delay decisions you do not have to make right now. Figure 2 summarizes this idea.