Have you ever heard the saying, “architects hate spaghetti?” As software architects, it is our responsibility to envision and design systems capable of supporting the cutthroat business models of this era. In that sense, it's fundamental to develop ways to evolve our application architecture to match business concepts and processes correctly. Otherwise, the architecture won’t be structurally sound, and we’ll have to deal with a dreadful “spaghetti architecture”.
In this blog post, I’ll share some of the best practices you should follow to build a structured and scalable application architecture while avoiding turning your systems into a spaghetti bowl. This article is based on a recent TechTalk on the same topic, Web and Mobile Architecture with Architecture Dashboard. For a more detailed discussion, I invite you to take a look.
Let’s get started!
Why Is Application Architecture So Important?
Application architecture includes all the software modules and components, internal and external systems, and the interactions between them that constitute an application. A well-structured application architecture ensures that your apps can scale as the business demands, meeting the intended business and user requirements while ensuring that all the concepts are correctly isolated and with sound dependencies among each other.
Suppose you ignore the architecture side of applications as you change and add new requirements to your software project. You’ll eventually end up with a spaghetti architecture, a labyrinth of unmanageable synchronization and dependencies between different parts of your application.
The problem of a spaghetti architecture is that it results in several issues, being the main ones:
- Poor service abstraction: Not correctly isolating and abstracting services around core business concepts spreads business rules across different systems, making code reusability very little structured and even impossible;
- Unmanageable dependencies: When components are not correctly isolated from each other, updating or replacing a system has a snowball effect, meaning that changes in one part impact all its dependencies;
- Inflexible, slow-moving legacy systems: Quickly adapting a legacy system to business changes becomes difficult. If the system is complex and inflexible, changes can take a long time. And if the technology is obsolete, the accumulation of core information and systems’ dependencies over time can hinder any replacements.
And that is why architects hate spaghetti. So, how do you avoid it?
Best Practices to Build a Scalable Application Architecture
The key to build a scalable and reliable application architecture is to base your architecture on strongly-defined tenets and well-established foundations. This way, you can support rapid growth and massive scalability, while avoiding deployment nightmares, higher code maintenance costs, and keeping up with the business needs.
To do so, you need to start with an architecture design, which will help you:
- Drive consensus among all players
- Support planning
- Facilitate change
- Manage complexity
- Reduce risks
- Minimize technical debt—which is the end-game of any well-designed architecture.
So, how do you create a future-proof architecture design? Let me introduce you to the Architecture Canvas, a framework to support and speed up the architecture design that we follow at OutSystems.
Introducing the Architecture Canvas
The Architecture Canvas is a multi-layer framework that promotes the abstraction of reusable services and components while preserving independent lifecycles, and minimizing the impact of changes, making your architecture easier to maintain and evolve.
Here’s how it looks like:
From the bottom to the top:
- Foundation layer: In this layer, you implement all reusable non-functional requirements, such as services to connect to external systems, or to extend your framework using libraries of reusable UI patterns, and themes, for example. All sorts of non-functional requirements.
- Core layer: On top of the foundational layer, you implement your core business services, including services around business concepts, business rules, business entities, business transactions, and business widgets.These services should be system agnostic and based on the foundation services to abstract any integration detail you might need. It’s in these two bottom layers that you isolate all the reusable services or components.
- End-user layer: The top layer is where you support your users’ interactions through user interfaces and processes using the core and foundation services to support the user journey. Note that a module on this layer should never provide services to other modules to ensure total lifecycle independence.
Validating the Structure of Your Architecture
To ensure that your architecture is sound and that you don’t end up with either a monolith or a spaghetti architecture, you should follow a set of guidelines and recommendations. The following rules refer to strong references across modules or applications (for e.g. actions or blocks), so disregard loose references like Service Actions or Screen Destinations.
1. No Upward References Across the 3 Layers:
Given the structured layering, it is evident that we don’t want business agnostic foundation services to depend on core business concepts or any reusable service to depend on End-user interfaces. Furthermore, upward references tend to create a cluster where any two modules directly or indirectly linked have a circular dependency. Take a look at the example below:
Module B can reach module A indirectly and module A can also reach module B indirectly. So, we have a cluster of interdependent modules. And if you have another end-user module (EU2) that is legitimately consuming the core service B, it becomes dependent on this entire cluster. Consequently, not only its runtime gets an unnecessary large footprint, but it also becomes impacted by changes made in modules it should not even be aware of.
2. No Side References Among End-Users:
The end-user modules should never provide reusable services to ensure they are correctly isolated, allowing the end-users to have different life cycles. See the example below:
If end-user 1 consumes end-user 2, not only it can’t be independent of EU2, but it can’t be independent of the remaining hierarchy below it either.
3. Avoid Circular References Between Core and Foundational Modules:
If you follow the previous two rules, you don’t have to worry about cycles among end-user modules. A cycle is always undesirable since it brings an expected impact on how to manage code. A cycle between modules indicates that the concepts are not correctly abstracted.
In the example below, one of two things should happen: 1) either A and B are so strongly connected that they should be part of the same module (e.g., Order and Order Line) or 2) one of the dependencies should be broken by placing the logic in the right place, according to the expected relation between concepts. For example, Contracts depend on Customers, but Customers should be able to exist without any reference to Contracts.
4. Extra Recommendations:
- Core modules shouldn’t have front-end screens: If you’re implementing a service, you might want to add some screens to make unit tests. But as a developer, once you test your code, you should get rid of test screens. If for some reason, those test screens are useful to support any kind of regression test or BDD testing, they should be moved to an end-user testing module. One of the main dangers of keeping test screens on your core modules is that they tend to be anonymous screens for testing purposes, and if they end up in production, they open a security breach.
- All entities should be exposed as read-only: This recommendation makes sure you’re not allowing consumers to use the crude actions to create, update, or delete records in the database directly because the core service should abstract the business transactions, taking care of validations, normalizations, and side effects like auditing or integrating with another system. Implement all your business transactions as public actions, to expose safe and correctly abstracted services to consumers.
- Avoid business logic on the foundation layer: Sometimes, people tend to implement business rules in this layer, but it should be business agnostic to make sure it can be reused in any domain of application.
- Don’t add core business entities at the foundation layer: Again, to be business agnostic, foundation modules should not have business-related entities. They can, however, have non-business entities to support some non-functional requirements of your applications. For example, if you need to create a generic service to audit all your transactions, you can create an Audit entity. The audit itself is not the business of your company; the business of your company is selling a Product or provisioning a new Customer or changing a Contract.
Application Composition Using the Architecture Canvas
Before going through the application composition, a quick disclosure: in this context, “application” doesn’t have the same meaning that we usually give it in a business context.
At OutSystems, we use the term “application” to refer to the set of modules defined in Service Studio — OutSystems development environment — that constitute the minimal deployment unit for LifeTime — OutSystems console for managing all your environments, applications, IT users, security, and app lifecycle. That said, what you promote from Development to Quality and from Quality to Production, are not single modules, but applications.
To identify which layer the application corresponds to, you should look for the topmost layer of the modules inside the application, meaning if the uppermost layer is an end-user module, for instance, then this is an end-user application.
Now, with applications into place, you should follow a set of rules to ensure a correct architecture.
Rule #1: Start with the Architecture Canvas Guidelines for Modules
Layer your modules correctly following the guidelines defined above.
Rule #2: Isolate Common Services
When the modules are put correctly into place, it’s time to go after the applications. Use the same principles here. So, if you have a module on “end-user application 2” consuming a module on “end-user application 1”, you should isolate the common core application to avoid dependencies. This way, you support both applications. The minute you want to share something, you need to isolate the common services in common applications.
Rule #3: Don’t Mix Owners
Having more than one owner in an application results in complex deployment management as the accountability for what has been changed becomes unclear. Promoting ownership is key. If it’s impossible to concentrate the ownership of an application, consider splitting it in a way that the ownership is clearly defined.
Rule #4: Don’t Mix Sponsors
Just like owners, sponsors have different demands and different bases. Let’s use an example: imagine a portal that allows the simulation of different insurance lines of businesses. If all lines of businesses are under the same application, any change done in one (for example, in auto) cannot be independent of the other. So, the slowest line of business will dictate the release cycle.
By creating separate applications per line of business, each one can determine the pace of each own delivery. Once that’s clear, you should identify and isolate whatever is shared among the different sponsors, and what should be governed together.
Want to See the Architecture Canvas in Action?
Then I invite you to join me in my recent Tech Talk Web and Mobile Architecture with Architecture Dashboard. In this session, I’ll show you how to design an architecture following the canvas principle and some of the best practices you should have in mind to avoid spaghetti architecture, using a real-life example. I’ll also go over the Architecture Dashboard, a tool to help you evaluate what you have done. Hope to see you there!