Arguably one of the most fun aspect of being a Software Engineer is the need for constant creativity. There are many ways to tackle a given problem with different trade-offs. Some trade-offs are performance related: should I employ the algorithm that uses more computation or the one using more memory? Others are around the structure of the software: should I build a reusable library or duplicate some of the code? There is no one answer that applies to all cases but there are some guidelines that we can use to help make the right choice in a given context.
The Flexera Architecture Manifesto provides some of these guidelines: they are high level considerations that can help drive the design of software to make it more efficient over time. When considering multiple options these principles can help discern the pros and cons of each approach and therefore pick the best option at hand.
The manifesto is broken down into ten principles. Most of these principles are fairly broad and apply to any kind of software, some are more suitable to a modern cloud based microservice style architecture which is the type of architecture that Flexera is embracing. I’m sure most of you will have come across some of these principles in other places but at the very least I hope this provides a good refresher!
Principle #1: Only simple designs can scale.
Also known as KISS (Keep It Simple Stupid) this principle is arguably the most impactful on the practice of software engineering. The hardest thing to do in software is to keep things simple, embracing complexity can be fun but the output needs to be as simple as possible. The reasons are many and obvious: most of the cost of software engineering lies in maintenance and simple code is going to be an order of magnitude more efficient to maintain, support and extend. A good software engineer can deal with complexity, a great software engineer provides simple solutions to complex problems. There are a number of consequences that derive from this principle, and some of the following principles touch on them but when in doubt going with simple is almost always the right answer.
Principle #2: No architecture is better than the wrong architecture.
No architecture means no software. The point here is that we all have a tendency to jump on the first – obvious – solution when there is a good chance that there are better (simpler?) options. Obviously implementing a sub-optimal architecture can have a lot of downstream consequences as changing architecture usually implies a rewrite. A good rule of thumb I use all the time is to make sure that at least 2 other engineers agree with my architecture design. Another trick I use is to become my own “devil’s advocate”: as soon as I come up with an idea I take a contrarian view and consider all the downsides, this exercise usually results in a “v2” design that’s a lot better.
Principle #3: Base your thinking on reality and apply your thoughts to reality.
Another common trap when thinking about architecture design is to “over-abstract” problems. The devil is in the details and keeping designs grounded has a better chance of resulting in a good outcome. Algorithms are fun, a working software is better. I always start designing around a real use case, once I have an architecture in place I then apply it to as many other real use cases as possible. Sometimes thinking abstractly is helpful and can accelerate the design – the trick here is to always apply the results to concrete use cases and to be ready to accept that the beauty of our solution may have to be sacrificed to account for reality.
Principle #4: Favor composability over extensibility.
This principle is a direct application of #1. We all have been conditioned to avoid duplication at all costs. That’s mostly a good thing but like all things in life it also has a flip side. Shared code that embed generic logic that can be applied to solve multiple problems has a natural tendency to grow in complexity. Any time a new problem comes up that can be solved by adding “just one more configuration option” the complexity of the shared code grows. This means that all users of the code now have to deal with that complexity, worse more complex code also means more buggy code and these bugs – like the code – are now shared by all. The problem of ownership is also harder to solve with shared code. There is an alternative approach: keeping the code simple – making it solve just one problem with a crystal clear interface. Linux aficionados like to make the comparison with the Linux command line tools: each tool does something extremely simple, but scripts can combine them together to implement fairly complex behaviors. Keeping the code simple and modular allows others to benefit from it and compose that code into their own. In a microservice style architecture that composable behavior can be encapsulated in a microservice which means that there is also clear ownership.
Principle #5: Make no assumption.
OK maybe this should be “make as little assumption as possible” because it’s really difficult to make none. The point though is that the less our designs assume that something is true the more robust they are. Assumptions have a natural tendency to become wrong over time, conditions change and things that once were true aren’t anymore. A special case of this principle is around assumptions being made on how the software will be used. I’m sure we’ve all heard or said “the customers will never do that!” and much to our dismay they did! And just to be clear: the solution here is not more configuration options as this usually results in a poorer user experience (back to KISS).
Principle #6: Design capabilities – not functionality.
This principle is about writing elegant code: basing the design on “business” abstractions that represent real-life constructs helps keep the code consistent and simple over time. Behavior should be attached to these constructs such that when we’re faced with new requirements it should be clear as to what abstraction and what behavior we need to be update or add. In a microservice style architecture these abstractions and behaviors can be encapsulated in specific – well documented – microservices. Just to give an example of an anti-pattern here: adding a new action in the UI should not systematically result in a new function in the back-end code (and yes I am well aware that entire frameworks were once designed around that principle and have seen the disastrous consequences first hand). Instead whatever behavior that new button represents should be mapped to the existing abstractions and it should be clear what needs to be changed or updated.
Principle #7: It’s all about operations.
Software that sits in a source code repository isn’t that useful, software that runs and provides a service is. As most organizations embrace devops I’ve found that a challenging aspect of that transformation is for us developers to fully comprehend that the work we used to do is now only 50% of the entire work involved in designing, implementing and running software. It’s not just time, it is also fundamentally changing how we have to think about and how we design software. Devops forces us to think about failure modes, scalability, upgradability and other operational concerns that used to be the sole focus of a different group. On the flip side we are also now more empowered than ever to make the right choices (which also means that we are accountable and, on the hook, when things go awry). In my experience devops teams are happier and more excited about their work and the results speak for themselves, we saw a 35% increase in amount of work done (as measured by number of stories completed) when we transitioned to devops.
Principle #8: Favor choreography over orchestration.
Choreography is about having software modules (e.g. services in a microservice style architecture) emit and react to changes while orchestration is about having a special module coordinate activities between other modules. Choreography makes it possible to introduce new activities with minimal changes: the new activity can be implemented by a new module that subscribes to the proper changes. On the flip side handling errors is more complicated when using choreography as by definition each module acts independently. This principle is really about making sure that the corresponding trade-offs are considered when designing the architecture of a system. It is especially critical when working with distributed systems where “module” in this description can get replaced with “service”. Typically, a distributed system architecture is composed of clusters of orchestrated services that communicate asynchronously to other clusters.
Principle #9: Convince – do not coerce.
OK I’ll admit this may sound like it does not have a lot to do with software architecture, but we’ve all been guilty of pushing our ideas a bit too hard. Software engineers can have strong opinions and that’s not necessarily a bad thing however it should always be possible to argue a design based on merit. This is particularly important for people whose job it is to oversee these designs (e.g. architects). Taking the input of others into account generally leads to better software.
Principle #10: Communicate to innovate.
In the modern business world, the company with the best teams win - not the company with the best individuals. True innovation comes from cross-functional teams where engineers play a key role by providing a view on what is possible. As software engineer, we need to engage with the rest of the business and truly understand the challenges faced by customers and the business outcomes that they are after. Only then can we use our creativity and truly innovate.
The Perfect Software Engineer
One way to summarize the principles above is to describe a software engineer that applies all of them, the perfect software engineer if you will! Such an engineer would strive for simplicity (#1) and would evolve the design of the software starting with as few assumptions as possible (#2) (#5) and leveraging the input of other people including non-engineers (#9 and #10). The perfect engineer identifies a set of capabilities (#6) grounded in reality and continuously validates that set against actual use cases (#3). She designs a set of small components with clear interfaces (#4) that leverage choreography where appropriate (#8) and that are properly instrumented and built for operation (#7).
To conclude I’d love to hear what other principles you’d add to this list? Would you change or remove some of them? Let me know in the comments!
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.