Similes are often useful for understanding a concept by transference.
Consider homes and gardens. Each is driven by requirements - you may want a particular type for a particular purpose.
The garden structured by colour, or sunlight availability for example. And the house built to have a desired number of rooms, level of natural lighting, or certain utilities.
Similarly, Architecture organises the software in a structure, so that it may successfully fullfill the needs of the project. So the architecture defines the structure of, which realises the business needs ‘from’ the system.
The structure can include things such as what services a system provides, how it achieves interoperability with other systems, and the storage mechanisms a system uses.
Four dimensions of Software Architecture
- Architectural Characteristics: the aspects that an architecture needs to support
- Architectural Decisions: important decisions that have significant or long-term implications for the system
- Logical Components: the system’s functional building blocks, that describe how the functionality is grouped and how these groups interact
- Architectural Style: the physical structure that fixes the operational aspects of the system. It is a common mistake to focus exclusively on this aspect.
Each of these dimensions must property align with each other.
Architectural Characteristics
Here we ask business the question, ‘What is more important to you?’, and analyse the trade-offs that are implicit in the answers.
For example, when choosing a home, these might be:
| Characteristic | Trade-off |
|---|---|
| a peaceful neighbourhood | expensive |
| economically viable | small rooms & noisy neighbourhood |
With architectural characteristics an instance could involve:
| Characteristic | Trade-off |
|---|---|
| Performance | caching & concurrency mechanisms can increase complexity and make change riskier, tightly coupled components can reduce maintainability |
| Maintainability | use of layered architecture while easing long-term evolution can add overhead and reduce performance |
| Security | encryption will reduce performance |
Commonly referred to as non-functional requirements (NFRs) and ‘*-ilities’ (because they often end in ‘-ility’), such as:
- Scalability: maintaining constant response times or error rates in the face of increasing users and requests
It boils down to asking, ‘What capabilities or characteristics are critical to my application?’. Does it need to be:
- Extensible: easily enhanced to support new features
- Resilient: able to survive failure in one part by keeping other parts functioning (even if at reduced performance)
- Interoperable: collaborating with other systems for data exchange or to complete business flows / use cases
- Feasible: to build, having accounted for time/budget/dev skills constraints in making architectural choices
- Agile: an amalgam of maintainability, testability and deployability, that influences how quickly it can respond to changes in the domain or business environment
Architectural Decisions
Architectural Decisions are a projection of the problem domain, and serve as structural guides to the design and implementation phases. While the characteristics are the key non-domain capability requirements, they get implemented as architectural structures. The architectural decisions reflect these structures and their inherent dynamics.
For example, a garden with a rainforest theme might have ferns and water bodies. Another with an ornamental plants theme, might have plants that propagate with less water.
Let’s look at an example of an architectural decision:
We will use an RDBMS for storing hierarchical data,
due to team familiarity with SQL, ACID transactions, indexing, and tooling, and easy integration with ORMs.
It easily caters to shallow (upto 3 levels) hierarchies, that are largely read as a whole.
The downside is, that while it is simple to understand, it requires recursive queries or joins for traversal, due to an adjacency list (parent_id) style of storage. This results in N+1 queries via the ORM, which, along with deeper hierarchies, impact performance. You can’t easily move a branch elsewhere either. And querying for all descendants (say members of a group) with a condition, becomes a problem.
Mitigation strategies might involve:
- using an RDBMS for core entities & caching hierarchies using Redis and precomputed tables
- write normalized and read denormalized or materialized views
- or switch to graph databases or document stores with embedded trees or search engines
The decision should document:
- expected hierarchy depth
- read vs write patterns
- frequency of restructuring
And another:
Decision: invocation of external collaborating systems must be done using async API calls.
Implementation Constraints: will need to use a Message Broker or implement a callback for capturing any response.
Logical Components (building blocks)
A logical component performs a function (e.g. payment processing, invoicing, reporting).
It:
- should have a well-defined role and responsibility in the system - a clear definition of what it does
- usually translates into namespaces (
yourapp.payment) and directories (yourapp/payment) for organising the code
A component might be considered a sub-domain of the business problem that the system addresses. It is a part of the domain.
Example:
graph TD
subgraph row1[" "]
direction RL
ELM["Evaluation Lifecycle Manager"] --> EB["Evaluation Builder"]
end
subgraph row2[" "]
direction LR
ERev["Evaluation Reviewer"] --> ELM
ERes["Evaluation Responder"] --> ELM
end
The ELM component is responsible for handling the lifecycle of an evaluation, progressing it from draft to submitted, and evaluation instances from assigned to completed.
References
- Gandhi, R., Richards, M., & Ford, N. (2024). Head First Software Architecture. O’Reilly.