Categories: Architecture
I’ve been watching an interesting presentation by Neil Ford on evolutionary architecture from 2016 (there is also a book on this topic). I find the talk itself a bit rambling/unfocused but there are some very interesting points there. This is of course related to his book which I have reviewed recently.
A more interesting talk on this topic, IMO, is Michael Nygard on Architecture without an End State - particularly the first 10 minutes. The rest is also interesting, but discusses enterprise-scale issues rather than evolutionary architectures.
Rebecca Parsons also makes a good point: (paraphrased) “you don’t know which framework you will be using in 12 months, so how do you do architecture?”.
Look at any complex IT system, and ask the question “if we were rebuilding this today, would we choose the same design?”. I suspect that the answer would almost never be yes. Technology changes rapidly, as do methodologies. And companies as a whole also learn and evolve, making choices possible today that weren’t possible a few years ago no matter how much they were wanted.
This continual change in the desired architecture therefore suggests that we should architect systems to support change. In particular, they should support incremental changes, allowing pieces to be modernised without massive cross-component modernisation projects. This applies to individual code-bases, to build-tools, infrastructure, and processes.
The core of the idea is: build loosely coupled systems - a theme that is not new. However there is a little more to it than that.
In a single monolithic application, this can mean choosing technologies such as OSGi for the JVM, where different modules of an application can rely on different versions of the same libraries - or on different libraries which have common transitive dependencies in different versions.
It can also mean choosing something like microservices, where each deployable artifact can be implemented in a different language and use different libraries.
It also means allowing different versions of artifacts to be deployed in production in parallel, eg V1 and V2 of some core service, allowing other components in the system to gradually migrate from one to the other on their own schedule. This isn’t trivial, requiring dealing with things like service discovery (using components need to be able to find the distinct services based on version-number) and data ownership (when data is updated via V2, what happens to queries against V1?).
It also means having support from infrastructure, eg allowing different applications to use different databases or versions of databases. An inflexible platform that dictates that all applications must use a specific database type and version does not support evolution of the architecture.
Having an architecture in which multiple versions of software can be deployed in parallel not only supports “modernisation”, it also supports experimentation where variants of code can be deployed into production and made accessible to specific groups of users (a/b testing, “hypothesis-driven development”).
A related topic is knowing when the architecture needs to evolve, or alternatively when a particular evolution has undesired side-effects. A more flexible architecture can lead to rapid change that is not always completely supervised/analysed before deployment and so some automated way of ensuring things don’t get out of hand can be useful. This can be measured with architectural fitness functions - tests or metrics that regularly verify that the system fulfills specific non-functional requirements. And if you have specific hard requirements (governance) that must be applied to code, then there is a bad choice (manual approval of all code to make sure it complies) or a good choice - automate that verification so code which violates the rules will not compile or will not deploy successfully. Only the second really scales.
Overall point: predicting the future is hard. Changes might come out of development processes, languages, libraries, tooling, data-management, security, and infrastructure. Not to mention business requirements/environment and legal obligations. So instead of predicting, design systems that can effectively react and adapt (proactive and reactive).
We’re quite used to the idea of having code designed for evolution, aka refactoring. There are many different patterns for decoupling code types and modules from each other. However applying this to a full architecture requires some different patterns and is somewhat newer. It also touches far more people than just developers; in particular it draws in those responsible for build, testing, and deployment. What if a new build-tool needs to be integrated - how much impact does that have on the overall system? Can things be done ahead-of-time to minimise such impact?
Important contributors to evolutionary architectures include:
- independently deployable services
- separate database per service
These are of course the core of the microservice pattern. And these strongly suggest the STOSA approach - each service belonging to a cross-functional team. This isn’t a prerequisite for an evolutionary architecture (one which can rapidly react to change) but seem to be nicely compatible with it.
Evolution is of course far easier with a good deployment and release process. If you want to deploy different variants of software in parallel, you’ll need parallel pipelines - and if creating new pipelines is hard, that interferes in developing these new variants.
Mature DevOps also helps; when evolving a component to a more modern state, its external dependencies (the processes it interacts with) may also evolve - eg new database, new metrics. If deploying these new dependencies requires change-tickets, planning, and coordination with other teams then that acts as a brake on such evolution. The most efficient solution is a cross-functional team where infrastructure and monitoring changes can be implemented by a single group with minimal bureaucracy.
Neil’s talk does go off onto tangents about some unrelated topics such as ACID vs eventual consistency. Also layered architectures and open/closed layering are only somewhat related to the topic of architecture evolution. Microkernel design too, and HATEOS. Yes, these topics influence the amount of coupling between systems, and therefore their independent “evolvability” but the point is somewhat over-laboured. The stuff about continuous delivery and “pulling pain forward” is good practice, but the link to evolutionary architectures is not clear to me. And the points about avoiding bottlenecks in distributed systems seem completely off-topic. The conway’s law stuff is vaguely linked to evolution via the idea that it is most efficient to evolve suites of artifacts which implement a (vertical) domain; if you instead try to for example evolve a horizontal/cross-cutting concern such as “the database platform” for every component in the company, or “the version of library X” for every component in the company, the effort and risk is much higher.
If you have a system that evolves effectively, that supports minimal viable products better. Build systems with only the minimal design/infrastructure that they need, in the knowledge that upgrading them later if needed isn’t a big hassle. This only works when the new system is minimally coupled with its surroundings.
The same approach of “decoupling infrastructure per component” (ie not having a standard platform for infra) which supports evolution also supports components in choosing the “best fit” for those infrastructure components. When stuff is shared, that shared infra must have a feature-set that satisfies all possible use-cases, ie is overkill for some.
As well described in this talk on Scrum vs SAFE, architecture is about enabling a team to scale. If a task can be done by 1-3 people, then they should just do it - no architecture is needed. But beyond that, you can’t just tell people to “read the requirements, pick a part to implement, and go to it”. Instead there needs to be high-level “architectural” structure so that as each works on their part, no duplication occurs and the parts fit together afterwards. And the larger the system, the more such structure is needed. This structure also needs to be maintained just like the code; feedback loops tell you in which way the architecture needs to evolve to make the code fragments implemented by different people and teams fit together. The structure can be imposed (“do it like this”) or can be guidelines (“try these approaches”) or can be technically enforced (“pick any solution that passes the automatically enforced rules”) depending upon the sophistication of the next layer down. Or as proposed in the talk, the architect ensures that everyone has the same understanding of the big picture, so that decisions later made within the teams are consistent with it.
The classic book Domain Driven Design by Eric Evans also talks extensively about architecture (at least the model of the business problem being addressed) being iterative and evolutionary. There are far too many places in which this is discussed to reference them all, but a summary of the author’s view can be found in pages 490-497 (starting with the section titled “Who Sets the Strategy?”) which specifically address the roles of “architect” and “developer”, and the importance of feedback and evolution of the design.