Designing System Architecture

Putting it all together in a grand architecture.

gohkhoonhiang
Apr 27, 2023 | 8 mins read

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

UI layer iteration 1.

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

UI layer iteration 2.

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

UI layer iteration 3.

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

UI layer iteration 4.

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

Interaction between UI and System.

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

Interaction between System and Persistence/DataStore.

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 overall system architecture.

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!