System Architecture Design
Previously, we have designed the data models, then the interactions between the user and the system, and also the interactions among the components of the system. Now it’s time to look at how the system will put together the user interface, business logic and data models.
At this stage, we are still not going to make any assumption about the implementation details, such as which UI framework should be used, or which datastore to integrate with. These are not needed yet for the purpose of designing the overall system architecture, as this is still an abstract design exercise.
User Interface
Since this is an app used by an actual person, we will need to have an user interface layer. This layer will accept input from the user, and forward the input to the system for processing. Upon receiving a result from the system, this layer will then render the output to the user. Let’s for now call them the “Input” and “Display” for a lack of better words. In the system layer, there will be a handler that handles the input from the user and then do some processing, and return a response that can then be used for display. Let’s call this the “InputHandler” for now.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction LR subgraph UI Input Display end subgraph System InputHandler end Input-->InputHandler InputHandler-.->Display end
For the “Input”, we do not know yet what is the mechanism to receive user input, whether is it going to be via a web form, or command line, or desktop UI. For this reason, we will put an abstraction layer between the part that takes the actual user input, and the part that sends the input to the “InputHandler”, let’s call that middleman the “Adapter” for now, which adapts the user input into a format that is accepted by the “InputHandler”.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction LR subgraph UI Input-->Adapter Display end subgraph System InputHandler end Adapter-->InputHandler InputHandler-.->Display end
The purpose of this “Adapter” is so that we can easily change out the user-facing component without having to rewrite all the code that is meant to interface with the “InputHandler”. We can even allow for multiple input mechanisms at the same time, and all of them will just go through the “Adapter” to send the input data to the system.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction LR subgraph UI Input1-->Adapter Input2-->Adapter Input3-->Adapter Display end subgraph System InputHandler end Adapter-->InputHandler InputHandler-.->Display end
Since we could possibly have multiple input mechanisms, it is natural that there will be multiple display mechanisms that correspond to each input mechanism. Again, we will need a middleman that transforms the response from the system’s “InputHandler” to a format that each “Display” can render to the user. Let’s call this the “Transformer” for now.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction LR subgraph UI Input1-->Adapter Input2-->Adapter Input3-->Adapter Transformer-.->Display1 Transformer-.->Display2 Transformer-.->Display3 end subgraph System InputHandler end Adapter-->InputHandler InputHandler-.->Transformer end
At this point, the UI layer is pretty complete, let’s move on to work on the details of the system layer.
System
We loosely call the layer that interfaces with the UI the “System”, because we have not made a decision regarding how it is implemented, like, we don’t call it the “Web Server” yet because we don’t know if it’s going to be a web app. When we finally make the decision, we can then refine the name of this layer. For now, let’s go with “System”.
Now, the “InputHandler” can’t be doing everything on its own, it should delegate some tasks, such as business logic processing, datastore access, so that we have a clearer separation of concerns. Let’s create the various components that will handle each of the functions mentioned earlier.
Note: For simplicity, we will not show the full UI layer diagram, but only the components that directly interact with the system layer.
Naturally, the flow of data should go from the “InputHandler”, which might perform some input validation, then to the “BusinessLogic”, which will perform the actual processing on the input data, while possibly also interact with the “DataAccess” to retrieve or write data from and to the persistence layer.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction LR subgraph UI Adapter Transformer end subgraph System direction TB DataAccess-.->BusinessLogic BusinessLogic-->DataAccess BusinessLogic-.->InputHandler InputHandler-->BusinessLogic end subgraph Persistence DataStore end DataAccess-->DataStore DataStore-.->DataAccess Adapter-->InputHandler InputHandler-.->Transformer end
Persistence
Since we mentioned the persistence layer, let’s look at how the system layer actually interacts with the persistence layer.
Why do we need a “DataAccess” component between the “BusinessLogic” and the persistence layer? The reason is that we have not made a decision regarding what persistence mechanism will be used. It could be a relational database, or NoSQL database, or even simple files storage. The “DataAccess” then serves as an abstraction that handles the mapping of CRUD actions to the underlying persistence functions.
Similar to the abstraction of “Adapter” for input and “Transformer” for display, we could possibly allow for different persistence mechanisms to exist at the same time. We then just need to write the different CRUD interfaces for each persistence mechanism, and the “BusinessLogic” can use any one of them where applicable.
Note: UI layer diagram omitted for simplicity.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction TB subgraph System DataAccess-.->BusinessLogic BusinessLogic-->DataAccess end subgraph Persistence Persistence1 Persistence2 Persistence3 end subgraph DataStore rdb[(RDB)] nosql[[NoSQL]] fs[/filesystem/] end end Persistence1-->rdb rdb-.->Persistence1 Persistence2-->nosql nosql-.->Persistence2 Persistence3-->fs fs-.->Persistence3 Persistence1-.->DataAccess DataAccess-->Persistence1 Persistence2-.->DataAccess DataAccess-->Persistence2 Persistence3-.->DataAccess DataAccess-->Persistence3
Putting Them All Together
Now that we have the lower-level details of each layer, let’s put them all together to have a big picture view of the system architecture.
%%{ init: { 'theme': 'base', 'themeVariables': { "primaryColor": "#f4e3d7", "primaryTextColor": "#502d16", "primaryBorderColor": "#784421", "lineColor": "#784421", "secondaryColor": "#a05a2c", "tertiaryColor": "#deaa87" } } }%% flowchart TD subgraph APP direction TB subgraph UI Input1-->Adapter Input2-->Adapter Input3-->Adapter Transformer-.->Display1 Transformer-.->Display2 Transformer-.->Display3 end subgraph System direction TB DataAccess-.->BusinessLogic BusinessLogic-->DataAccess BusinessLogic-.->InputHandler InputHandler-->BusinessLogic end subgraph Persistence Persistence1 Persistence2 Persistence3 end subgraph DataStore rdb[(RDB)] nosql[[NoSQL]] fs[/filesystem/] end end Adapter-->InputHandler InputHandler-.->Transformer Persistence1-->rdb rdb-.->Persistence1 Persistence2-->nosql nosql-.->Persistence2 Persistence3-->fs fs-.->Persistence3 Persistence1-.->DataAccess DataAccess-->Persistence1 Persistence2-.->DataAccess DataAccess-->Persistence2 Persistence3-.->DataAccess DataAccess-->Persistence3
The purpose of this exercise is to help us understand how the various subsystems of the app interact at a high level. This is important as it will guide us through how to draw the boundaries of various responsibilities so that there is clear separation of concerns.
The goal of separating the concerns to the appropriate component or subsystem is so that we can build appropriate abstraction layer between where 2 subsystems interface with each other. Once we establish the interfaces, we do not need to constantly worry about making changes that will break other parts of the app.
You might have noticed that the “Adapter”, “Transformer” and “DataAccess” components sound like Interface in some programming languages, and that’s actually correct. However, we have not made a decision regarding what framework to use, and the programming language of the eventual framework may not support Interfaces in the form we know it, so we will keep those as “concrete” component for now in our system architecture design.
Of course this is not bullet-proof, and there is always the possibility that we have not designed the interfaces very well, or that the separation is not done cleanly. Regardless, we should at least have a sound foundation to start with, and when we find out more about the possibilities or limitations of the implementation, we can refine the design accordingly.
This exercise has been useful for me in showing how the multiple components come together in a complex system architecture. Now that we have the skeleton of the app ready, let’s revisit the presentation of the app and design some wireframes!