Key Takeaways
- Software frameworks greatly amplify a team’s productivity, but in doing so they make decisions that are not always easy to see. When choosing a framework, consider how it makes decisions that affect your application’s architecture by considering how it handles Quality Attribute Requirements (QARs).
- Don’t let a framework take over your application. By making key decisions for you, a framework makes certain things easier, but at a cost: it also limits your flexibility in how you do other things, including how or whether you can use other frameworks.
- Frameworks are not easy to walk away from. They tend to affect the way that your applications look at your problem space. It’s the classic hammer-nail problem; for example: if you choose to use a rules engine, you start thinking about handling all conditional logic as the application of a rule, even when that’s not the best way to handle the problem.
- Patterns make decisions too. Understanding the design decisions associated with a specific pattern, as well as which decisions need to be made when implementing it, is a critical factor in using a pattern successfully.
- Architectural tactics can help you address QARs. You need to make architectural decisions to satisfy QARs and these decisions can be implemented by using architectural tactics. It is important to fully understand decisions and their implications before deciding to use tactics to implement them, and not to over-design by using unnecessary tactics.
In previous articles, we have explored how decisions are the foundation of architecting software systems. In “Software Architecture: It Might Not Be What You Think It Is”, we argue that software architecture is about capturing decisions, not describing structure, and in “Why You Should Care About Software Architecture”, we state that greater awareness of the implicit architectural decisions development teams are making, and forcing these decisions to be made explicitly, can help them make better, more informed decisions using the empirical data they gain from their sprints/iterations.
Among the most important architectural decisions a team will make are choices about the frameworks they will use, the patterns they will employ, and the architectural tactics they will use. Consideration of the benefits and limitations of each of these shapes the decisions the team makes and the resulting architecture of the system.
Frameworks, patterns, and tactics can help a team to more quickly develop a Minimum Viable Architecture (MVA) for their application, but there are potential concerns that the team will need to consider in using them, lest they create an MVA that results in substantial rewriting when implicit decisions made by the frameworks, patterns, and tactics are later found to be incorrect. Please see “A Minimum Viable Product Needs a Minimum Viable Architecture” and “Minimum Viable Architecture in Practice: Creating a Home Insurance Chatbot” for more details on MVAs.
Software Frameworks
Software frameworks are abstract, extensible code libraries that can be adapted to specific purposes by developers. Modern software systems could not be built without them, and they let developers focus on their unique value-added features without having to develop all the supporting software that every system needs. Frameworks can vastly improve productivity, but at a cost: you have to work the way they want you to work, and they can introduce undesirable side effects like hidden security vulnerabilities or hidden architectural weaknesses.
Don’t let a framework take over your application. Frameworks have a tendency to take over the application, even to the point that it colors the way the development teams look at the problem they are solving. Using a rules-engine framework, for example, is powerful if what you have to deal with are decisions based on data, but we have seen teams start to contort their application to make everything into a rule so that they can use some attractive feature of the framework. In doing so, they missed opportunities for solving certain problems in simpler and more straightforward ways.
Understand the decisions the framework is making for you. In the context of software architecture, a framework has the effect of making architectural decisions for you. The problem with most frameworks is that they are not explicit and transparent about the architectural decisions they are making. These decisions may be acceptable for your system, or they may not; it is up to you to do your homework so you can decide if the framework will work for you. This may extend to actually building some critical part of the system using the framework so that you can verify that it will meet your system’s QARs.
For example, an open-source, reusable framework such as the Spring framework can be used to develop a range of Java applications and has become very popular in the Java community. Since it is large and fairly complex, developers need significant training and experience to use this framework effectively.
Spring Boot is a Spring Framework extension that enables developers to rapidly create stand-alone, production quality applications that leverage the Spring framework, with much less training and experience. Spring Boot achieves this level of developer productivity by making a number of design decisions, including selecting the “best” configuration (according to the Spring Boot designers) and use of the Spring platform and third-party libraries.
Choosing a framework like Spring Boot can significantly accelerate application development and implementation, but developers need to understand the decisions that the framework implicitly makes for them, as they may need to adjust or even reverse them to ensure that the application remains sustainable as it evolves beyond its initial release. In the case of Spring Boot, these decisions include which configuration defaults to use, and which packages need to be installed for the dependencies required by the application.
Plan to stay current on the framework. Frameworks, like any other code, have inherent flaws including security vulnerabilities that may remain hidden for years. Unlike code they manage themselves, organizations usually depend on others to update the framework, which could be another company or the maintainers of an open-source project. Even when new framework versions are released, the organizations who use the framework must actively work to upgrade their applications to the latest version. Using old versions of frameworks is a major source of security vulnerabilities, imposing significant risks that remain hidden to the inattentive.
Don’t choose a framework because some big-name company uses it. Some people fall into a trap when they choose frameworks: because some big-name company uses the framework, or maybe even developed the framework, it must be good. No doubt it is good, in many respects, but that big company may not have the same context and challenges that you have. Whether you decide to evaluate the quality attributes of the framework or not, you are making an architecturally-significant decision. It’s best to do this with your own data rather than trusting that someone else has done the work for you.
Don’t let your team’s skills lapse. Because frameworks handle complex problems in such a transparent way, developers who use frameworks may lose, or never develop, the ability to understand or develop the code that the framework replaces. As a result, they may not understand the architectural choices that they are inherently making when they choose a particular framework.
As a result, using a framework cannot substitute for sound architectural design; developers making architectural decisions must understand the trade-offs the framework makes, and when those trade-offs may become unacceptable. Some of this knowledge can be gleaned from talking to other developers who use the frameworks, to understand the assumptions the framework makes and the limitations these assumptions may impose. Community knowledge may be sufficient to exclude certain frameworks from consideration, but the ultimate test of any framework is to test the system against its Quality Attribute Requirements (QARs).
Plan for replacing the framework. Understanding the decisions made when selecting the framework is especially important if the framework has to be replaced, such as is the case if the framework changes in undesirable ways or is facing end-of-life/non-support. Frameworks come and go, and failing to plan for obsolescence can leave a development team facing costly rewrites or replacements. Even commercial frameworks can be end-of-lifed due to mergers or changing business conditions. Knowing the cost of adapting a system to framework alternatives is always an important factor in making architectural decisions.
Programming languages have become frameworks-in-hiding. It’s worth a moment to consider the programming language as a kind of framework. Originally, higher-level languages were little more than an abstraction of the underlying hardware, but they have gradually expanded in scope so that nearly all modern languages now include substantial libraries to deal with certain kinds of problems, and some are even optimized for dealing with certain kinds of problems. Even relatively “ancient” languages like COBOL impose on their programmers a certain perspective that, when it works, saves time and effort, and when it doesn’t work, creates more work and makes certain kinds of problems impossible to solve (try solving matrix algebra problems with COBOL, for example.) The choice of a programming language is one of the most architecturally-significant decisions a team can make.
Ecosystems represent an extreme case of frameworks
Ecosystems like Amazon Web Services (AWS) or Microsoft’s Azure provide entire families of frameworks that work together in a harmonious way. In choosing them, a team implicitly makes a large number of decisions about how they are going to work and how their system will solve a vast array of problems.
All comments we have made about frameworks apply to ecosystems, but to an extreme degree: teams need to understand what decisions are being made by the ecosystem and its frameworks because the cost of abandoning the ecosystem and choosing a different one will probably require a substantial rewrite of the application. Early in the development of the application, you need to decide whether you are ready to make that sort of commitment.
One thing to consider about ecosystems is that their ultimate goal, for the vendor, is to keep users of the ecosystem within the ecosystem. As a result, it’s virtually impossible to move to a different ecosystem without a substantial rewrite of the application. Once committed to an ecosystem, organizations also need to be wary of having the cost of using the ecosystem rise over time, since the vendor knows that the cost of leaving the ecosystem is very high. Entry-level pricing shouldn’t be assumed to last forever.
Patterns
Patterns are reusable, proven solutions aiming to solve common software design problems within a given context. Architectural patterns can be thought of as packages of architectural design decisions, and these decisions need to be fully understood when using a pattern. Understanding the context in which the solution works is essential. Patterns can be useful when applied in the right context. They can enable developers to build better software faster, by leveraging sound design approaches already in use. Perhaps even more importantly, they can provide a useful common language to describe software challenges and potential ways to address them, providing that the pattern definition and description are accepted by a majority of IT practitioners.
Patterns may be harder to use than you thought initially. Patterns are often “merely” conceptual; that is, they are not realized in code but are simply algorithmic in nature. This does not mean that they lack value, since sometimes having a new way to look at the problem is the key to developing a great solution. But when patterns are merely conceptual, the act of realizing them in code is sometimes quite challenging to do for the general case; patterns are generic in order to be reusable, and instantiating a pattern to create a design in a specific context requires significant experience.
In addition, Inappropriate use of a pattern may result in a needlessly complex architectural design and convoluted application code when problems are not sufficiently isolated; the more general a pattern, the more likely it is that the underlying problems cannot be isolated, which in turn makes the pattern less general and reusable. In addition, patterns may not take into account the impact of the solution they provide on some of the QARs such as scalability or performance, and may need to be complemented with appropriate tactics to address this shortcoming. Asking questions such as “is it helpful?”, “is it useful?”, and “is it testable?” helps evaluate a pattern for potential use.
Patterns make decisions too. Understanding the design decisions associated with a specific pattern, as well as which decisions need to be made when implementing it, is a critical factor in using a pattern successfully. As we mentioned in a previous article, the essence of architecture consists of the set of decisions that define and constrain the technical aspects of the product. These decisions exist regardless of which approach the team takes. Using a pattern results in the team making a number of design decisions, consciously or tacitly.
For example, the layered architecture pattern, a popular pattern with software architects, designers, and developers, divides the software system into units (“layers”) that can be developed and evolved separately with minimal interaction between the layers. The pattern itself does not specify which layers should be used or how many layers should be implemented. It does not indicate which technologies should be used to implement the layers, how the layers should interface with each other, or how the application code should be packaged and run. This pattern is often implemented as a 4 layer architecture (Presentation Layer, Business Logic Layer, Data Access Layer, and Data Store Layer), with the code packaged in 3 tiers (Presentation Tier, Application Tier, Data Tier) - see Figure 1 below.
Figure 1: Sample Layered Architecture Pattern Implementation
However, decisions such as how many layers, how many tiers, or which technologies should be used, are left to the implementation team, depending on the QARs that need to be met. As a result, using this pattern may result in different system designs, depending on these decisions.
Between a pattern’s concept and its implementation, things can get lost. This is not unique to patterns, but using patterns amplifies the problem: In the layered architecture pattern, the layers themselves seem quite clear, but there is often little in written code, and nothing in the executing code, that actually distinguishes or enforces the pattern; the layer is simply a concept in the mind of the developer. As a result, there is often little that can be tested to ensure that the pattern is enforced. If enforcing the pattern is important to the team, it will have to invent ways to evaluate adherence to the pattern.
Tactics
Tactics are decisions that achieve the implementation of one or more quality attribute requirements. They are much more specific than patterns, and simpler to implement, but may have side effects that need to be addressed. They provide an effective way to satisfy QARs, based on knowledge and experience gained over years by software architects and engineers.
Selecting appropriate architectural tactics. Selecting and applying architectural tactics is a proven approach to addressing specific quality attribute requirements. Architectural tactics are an established idea, which came from research at the Software Engineering Institute at Carnegie Mellon University (SEI/CMU), originally to address some of the shortcomings of architectural patterns. An architectural tactic is a design decision that affects how the system implements one or more quality attribute requirements. Tactics are often (but unfortunately not always) documented in catalogs in order to promote the reuse of this knowledge among architects.
For example, data distribution is an effective scalability tactic. Data distribution involves partitioning data for specific services, by splitting database rows using some criteria, for example, customer identifier. This tactic should be used for specific databases that may experience issues when trying to cope with large workloads. Focusing on database scalability first, since databases are often the hardest component to scale in a software system, is a good approach to addressing specific scalability requirements.
Architectural tactics help you address quality attribute requirements. Functional requirements are usually well documented and carefully reviewed by business stakeholders, whereas QARs may not be so well documented and carefully reviewed. They may be provided as a simple list that fits on a single page. They may not be carefully scrutinized and tend to be stated as truisms, such as “must be scalable” and “must be highly usable.”
However, our view is that QARs drive the architectural design. We need to make architectural decisions to satisfy quality attributes. Those decisions often are compromises, because a decision made to better implement a QAR may negatively impact the implementation of other QARs. Accurately understanding QARs and tradeoffs is one of the most important prerequisites to adequately architect a system. Architectural decisions are often targeted to find the least-worst option to balance the tradeoffs between competing QARs. Every choice has side effects that might, at some point, invalidate the decision. There are no "right answers", only decisions that are "adequate for a particular situation". These decisions could be implemented by using architectural tactics, and it is important to fully understand the decisions before deciding to use the tactics to implement them. It is also important to keep in mind Continuous Architecture Principle # 3 “Delay design decisions until they are absolutely necessary”, and not to over-design by using unnecessary tactics.
Conclusion
Don’t make too great a commitment to any particular framework, pattern, or tactic before you need to. Instead, leverage the MVA concept to make only the minimum set of decisions that you need to in order to implement the MVP, be aware of the decisions that each of these is making, and decide for yourself whether those decisions are right for you to achieve your QARs.
Frameworks, Patterns, and Tactics can be combined to some extent. For example, Spring Boot can be used to develop parts of a software system that uses the layered architecture pattern. Tactics can be used to address pattern shortcomings if a pattern does not take into account the impact of the solution it provides on some of the QARs. However, you need to keep in mind that both frameworks and patterns make decisions on your behalf, and some of those decisions may conflict with each other when you attempt to combine a framework and a pattern.
As you evolve the MVP, incrementally adopt frameworks, patterns, and tactics only as much as you need to evolve the product increment. Be especially wary of ecosystem-related decisions, as these are almost impossible to reverse without starting over. And throughout, don’t lose sight of your goal–validate whether your assumptions about the frameworks, patterns and tactics are valid and whether your decision to use them will help you satisfy your QARs