# Architecture

Formally, Software architecture is the "fundamental organization of a system, embodied in its components, their relationships to each other and the environment, and the principles governing its design and evolution" [IEEE 1471-200].

Less formally, architecture is a shared understanding of how the system is structured. Martin Fowler (2003) attempts to pin down the term in a couple of different ways:

Definition 1: “Expert developers working on a project have a shared understanding of the system design. This shared understanding is called ‘architecture’ and includes how the system is divided into components and how the components interact through interfaces.”

Definition 2: “Architecture is the set of design decisions that must be made early in a project [and that we would like to get right]”.

Architecture is the holistic analysis of a system, and how it's parts relate to one another. Instead of examining requirements in isolation, we instead want to look at the consequences of the structure itself, including the qualities that emerge from this structure.

Architecture can be said to address the intersection of business goals, user goals and technical (system) qualities. The architect needs to determine how to deliver the functional requirements in a way that also addresses these qualities, and other potential business needs (e.g. cost). This may very well include making tradeoff decisions ahead of time. e.g. a user may want a system to return the results of a query in less than 5 seconds, but the cost of doing this might be prohibitively expensive!

The benefit to a careful architecture is that we have a more stable initial design that reflects our project concerns, while still allowing for adaptability, flexibility and other desireable qualities. We'll discuss different qualities of a system below.

Architecture addresses overlapping requirements
Architecture addresses overlapping requirements

So, what qualities do we want in the software that we build? What are the goals of architecture?

# Qualities

# 1. Usability

A system must be usable for its intended purpose, and meet its design objectives. This includes both functional and non-functional requirements.

  • Functional: features that are required to address the problem that the software is intended to solve; desirable features for our users.
  • Non-Functional: qualities or characteristics of software that emerge from its structure. e.g., performance, reliability and other quality metrics.

This requires us to carefully ensure that we are meeting all reasonable requirements up-front, typically by collaborating with our users to define problems and solutions. See User-Centred Design.

# 2. Extensibility

You should expect to adapt and modify your software over a lifetime of use:

  • You will find errors in your code (aka bugs) that will need to be addressed.
  • You might find that requirements were incomplete, so existing features may need to be modified.
  • You might uncover new features that need to be implemented.
  • Your operating environment might change (e.g. OS update) which necessitates updates.

We need to design systems such that you can respond and adapt your software to these changes, effectively, and with the least amount of work.

Your design goal is not to deliver software once, it's to design in a way that supports delivering frequently and reliably over the life of your solution.

# 3. Scalability

Extensibility refers to handling new or changing requirements. Scalability refers to the ability to handle increased data, number of users, or features. e.g., an online system might initially need to handle hundreds of concurrent users, but could be expected to scale to tends of thousands over time. Scalability is challenging because you don't want to incur the deployment costs up-front, but instead you want to design in a way that lets you expand your system over time. This can include replacing modules with more capable ones at a later time, e.g., swapping out a low-performance database for something more performant.

# 4. Robustness

The system should be able to handle error conditions gracefully, without compromising data. This includes user input errors, errors processing data from external sources, and recovering from error conditions, e.g., power outages.

# 5. Reusability

Software is expensive and time-consuming to produce, so anything that reduces the required cost or time is welcome. Reusability or code reuse is often positioned as the easiest way to accomplish this. Reusing code also reduces defects, since you're presumably reusing code that is tested and known-good.

I see three levels of reuse. At the lowest level, you reuse classes: class libraries, containers, maybe some class "teams" like container/iterator.

Frameworks are at the highest level. They really try to distill your design decisions. They identify the key abstractions for solving a problem, represent them by classes and define relationships between them. JUnit is a small framework, for example. It is the “Hello, World” of frameworks. It has Test, TestCase, TestSuite and relationships defined.

A framework is typically larger-grained than just a single class. Also, you hook into frameworks by subclassing somewhere. They use the so-called Hollywood principle of “don’t call us, we’ll call you.” The framework lets you define your custom behaviour, and it will call you when it’s your turn to do something. Same with JUnit, right? It calls you when it wants to execute a test for you, but the rest happens in the framework.

There also is a middle level. This is where I see patterns. Design patterns are both smaller and more abstract than frameworks. They’re really a description about how a couple of classes can relate to and interact with.

-- Shvets (citing Erich Gamma), Dive Into Design Patterns (2019).

# Architectural Patterns

An architectural pattern is the high-level structure that describes how components are organized and structured. Similar to design patterns, an architectural pattern is a general solution that has been found to work well at solving specific types of problems.

We typically think of patterns as either monolithic, or distributed.

A monolithic pattern is one where the implementation describes a single physical topology (e.g. a self-contained system or application). Many applications that we use are monolithic by design. Note that the ability to utilize remote services doesn't contradict this definition: a monolithic application could still make remote calls to fetch data from a web service, or a database.

A distributed pattern is one where computation is distributed across different systems (e.g. computation is done remotely). These distinct systems need to communicate with one another, and coordinate the distribution and management of work. This is typically done over a network connection.

Different architectures will exhibit different properties. This means that there is no "perfect" architectural pattern! You need to marry your requirements to an architecture that will adequately support them. Ideally, your architecture would also meet the qualities that we identified earlier.

Having said that... we will only talk about a couple of styles, and we'll gravitate towards a pattern that should work well for most applications.

# Big Ball of Mud

Architects refer to the lack of any discernible architecture structure as a Big Ball of Mud.

A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair. Information is shared promiscuously among distant elements of the system, often to the point where nearly all the important information becomes global or duplicated.
-- Foote & Yoder 1997.

Big ball of mud
Big ball of mud
From Building Evolutionary Architectures by Neal Ford, Rebecca Parsons, Patrick Kua

A Big Ball of Mud isn't intentional—it's the result of a system being tightly coupled, where any module can reference any other module. A system like this is extremely difficult to extend or modify.

Quality Score Comment
Usability Medium It's possible to hack something together once
Extensibility Low It's difficult to modify without introducing errors
Scalability Low It's probably not very efficient
Robustness Low It's likely not very reliable
Reusability Low No way that you can untangle this!

This obviously won't work. We need something that is extensible and scalable but doesn't sacrifice our ability to design interesting features.

# Monolithic

This is the simplest approach we can take: we just split our application logic into a couple of distinct classes/areas of responsibility. This is also known as a layered architecture.

Standard layers in this style of architecture include:

  • Presentation: UI layer that the user interacts with.
  • Business Logic: the application logic, or "business rules".
  • Persistence Layer: describes how to manage and save application data.
  • Data Store: the underlying mechanism to actually save data.

A simple monolithic architecture, with a small separation of concerns
A simple monolithic architecture, with a small separation of concerns

The characteristics of this approach:

  • Each layer is a module, containing classes with similar functionality.
  • There is no limit to the number of layers, although the four illustrated here are common.
  • Each layer provides services to the layers above it.
  • Dependencies are top-down only. e.g. the UI depends on the underlying business layer, which depends on the persistence layer and so on.

The major characteristic of a monolithic or layered style is that it enforces a clear separation of concerns between layers: the Presentation layer doesn't know anything about the application state or logic, it just displays the information; the Business Logic layer knows how to manipulate the data, but not how it is stored and so on.

Data is passed back up the layers through dependency inversion: the use of interfaces to decouple the lowest layers from the rest of the application.

Quality Score Comment
Usability Medium It's probably fine for the initial implementation only.
Extensibility Low It's difficult to modify without introducing errors
Scalability Low No way to handle more data.
Robustness Low Changing it is likely to break something.
Reusability Low No way that you can untangle this!

A layered architecture is an extremely common way to build an application! It falls out of some organizational styles quite naturally (see Conway's Law).

  • Front-end developers and designers work on the Presentation (UI) layer.
  • Back-end and database developers work on the Persistence layer.
  • Full-stack developers integrate these layers.

# Clean Architecture

Layering our architecture really helps to address our earlier goals (reducing coupling, setting the right level of abstraction). Additionally, it provides these other specific benefits:

  • Independent of frameworks. The architecture does not depend on a particular set of libraries for its functionality. This allows you to use such frameworks as tools, rather than forcing you to cram your system into their limited constraints.
  • Testable. The business rules can be tested without the UI, database, web server, or any other external element.
  • Independent of the UI. The UI can change easily, without changing the rest of the system. A web UI could be replaced with a console UI, for example, without changing the business rules.
  • Independent of the database. You can swap out Oracle or SQL Server for Mongo, BigTable, CouchDB, or something else. Your business rules are not bound to the database.

So what's wrong with using a layered architecture? It's too specific, and assumes that we are ultimately pushing data between a top-level UI layer and a bottom-level persistence layer. Modern software requires more flexibility.

Clean architecture is an alternative style commonly used with modern mobile and desktop applications, which generalizes a layered architectural model. It is also a domain-driven design that focuses on use-cases and addressing user needs, and aligns well with User-Centred Design (UCD).

Clean Architecture is derived from other modern approaches to architecting systems (described in Martin 2017). These include:

  • Hexagonal Architecture, developed by Alistair Cockburn, and adopted by Steve Freeman
  • DCI from James Coplien and Trygve Reenskaug
  • BCE, introduced by Ivar Jacobson

This diagram represents different layers of software, each with their own responsibilities. The following guidelines help determine how layers interrelate.

# Dependency Rule

The dependency rule describes relationship between layers: nothing in the inner circle can know about the outer circles. Dependencies are only allowed to go from the outside to the inside; outside classes can only refer to the same or a more inner layer.

This means that the domain objects (innermost layer) don't directly reference or use any external frameworks (outermost layer). External frameworks and libraries are pushed to the outside of the architecture to reduce coupling to these frameworks! This allows for greater flexibility later, if we need to replace one of them, e.g., switching to a different database implementation.

# Layers

A clean architecture is like an onion: layers spread out from the core entities. Here's what each layer does.

# Entities (Domain Models)

These are data classes that reflect your problem domain, e.g. classes like Customer, Invoice, Journal, Note. These are the core classes in your application and unlikely to change from external pressures or from the addition of features (which is why we can have everything else depend on them!)

What differentiates these from use cases is that enterprise entities can span components or services. If you are building a single application, then these are the core data classes.

# Use Cases (Application)

These are application-specific classes that build on the core entities. They facilitate data flow to and from the entities. This is also where you would implement core application logic (that wasn't handled by the outermost layers like the UI framework).

# Controllers (Infrastructure)

These are interface adapters that map data from the entities or use cases, to a format that is required by the external layers. For instance, this layer might add the model-view-controller abstraction for the user interface, or convert data from classes into a format that is suitable for storing in a database.

# Interfaces (Frameworks)

These are frameworks that provide services. Typically, these services are provided for you and you merely need to interface with them.

This design addresses our earlier concerns by providing a separation of concerns between each layer. This allows us to swap databases, user interfaces and other layers as needed.

# Dependency Inversion

The diagram above shows flow-of-control: which classes call into which other classes. In this case, outside classes call into inner classes. This also represents the source code dependencies.

The way we handle source code dependencies is with dependency inversion. This is meant to keep maintain the distance between high and low level modules:

  • High-level modules should not import anything from low-level modules. Both should depend on abstractions (e.g., interfaces).
  • Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.

# Source Code

How do we accomplish this in code?

  • We set up packages to separate classes into modules.
  • We use interfaces to describe the relationship between classes.

For example, we would set up a source code structure like this, where each subdirectory is a package.

project
├── application
├── database
├── entities
├── presentation
└── service

We will see examples of using this architecture when setting up our edge dependencies, e.g., database, user interface.

# Why do we do this?

  1. It reduces coupling by carefully controlling dependencies.
  2. It encourages cohesion since layers have a specific responsibility.
  3. External dependencies are pushed to the outermost layers, which reduces dependencies on specific frameworks or specific implementation dependendies. e.g., it is easier to swap out user interface frameworks, or keep your application decoupled from any specific implementation.
  4. Reducing dependencies makes it possible to test classes at each layer independently! We'll see this when we discuss unit testing.

In some ways, a clean architecture can also be described as a generalized layered architecture. We'll use these diagrams interchagably as we build out some of our other systems.

Clean-layered
Clean-layered

# Last Word