Getting Architecture Right: Part 1
What is it and how do we need to think about it?
We’ve got to cut through the verbal clutter to get software architecture right!
When we discuss software architecture, it is useful to understand that the term is often used for more than one kind of thing. Macro architecture, architectural patterns, inter-module communication, and programming paradigms serve necessary, but different, purposes in the design, implementation, deployment, and operation of software systems.
Macro Architecture
Macro Architecture (sometimes called high-level architecture) is where the primary concern is establishing the foundational attributes and principles that guide the selection of architectural patterns, detailed design, and implementation.
This level of architecture is crucial for setting direction and ensuring that the system remains aligned with its intended goals throughout its lifecycle.
The following macro architectures represent two of the better options for business applications:
Clean Architecture
Clean Architecture is a design philosophy that emphasizes separating a system into distinct logical layers (like an onion), each with its own set of responsibilities. The core idea is to create an architecture that is independent of frameworks, user interfaces, databases, or any other external factors. It prioritizes the central business logic and domain entities, ensuring that these core elements are insulated from changes in technology or delivery mechanisms.
This architecture promotes high maintainability, testability, and scalability by allowing developers to modify or replace outer layers (UI, database) without affecting the core business logic.
Lean Architecture
Lean Architecture is an approach that focuses on simplicity and efficiency, minimizing waste and unnecessary complexity in the system. Inspired by Lean principles from manufacturing, this architecture emphasizes delivering only what is necessary to meet user needs, avoiding over-engineering, and reducing redundant processes.
This architecture supports continuous delivery and integration, enabling rapid development cycles and quick adaptation to change. It fosters a mindset of building only the essential features — ensuring that the system remains adaptable, cost-effective, and responsive to customer feedback.
Both Clean Architecture and Lean Architecture aim to create systems that are maintainable, adaptable, and focused on delivering value — but they approach these goals from different perspectives.
Clean Architecture emphasizes the separation of concerns and protection of core business logic, while Lean Architecture focuses on efficiency, simplicity, and responsiveness to change. They both guide the implementation of architectural patterns.
Architectural Patterns
Architectural patterns are high-level, reusable solutions to common architectural challenges, providing logical templates for organizing and structuring software systems in a way that addresses specific concerns and promotes best practices. Their goals are:
- Simplification: They simplify the design process by providing a starting point and a set of principles to follow.
- Consistency: They ensure that the system design is consistent, maintainable, and aligned with best practices.
- Problem-Solving: They address common challenges in software architecture, such as scalability, flexibility, and resilience.
There is an unfortunate tendency to choose architectural patterns for an application without first understanding and choosing an appropriate macro architecture. It is also rare that one single architectural pattern can satisfy all the requirements of all the different facets of an application system.
Examples of common architectural patterns are:
Monolithic
A monolithic pattern is a unified system where all the components are tightly integrated and operate as a single unit. The entire application, including the user interface, business logic, and data access, resides within a single load module.
This pattern can simplify initial development and deployment but can become cumbersome to scale and maintain as the system grows, since changes in one part may affect the entire system.
Layered
The layered pattern organizes the application into distinct layers, each responsible for a specific aspect of the system’s functionality, such as the presentation layer (UI), business logic layer, and data access layer. Each layer interacts only with its adjacent layers, promoting separation of concerns and modularity.
This pattern can make a system easier to understand and maintain, though it can become inflexible and challenging to scale for complex systems.
Event-Driven
The event-driven pattern centers around the production, detection, and reaction to events. Components within the system communicate through the generation and consumption of events, allowing them to operate independently and asynchronously.
This pattern can be useful for systems that require real-time processing or need to handle a high volume of interactions, but it can introduce complexity in managing workflows and ensuring consistency.
Service-Oriented Architecture (SOA)
The SOA pattern divides the system into a collection of loosely coupled services, each providing specific business functions and communicating over a network using standardized protocols.
These services should be self-contained, promoting reusability and scalability.
SOA facilitates integration across different platforms and technologies, but it can be complex to manage due to the number of services and their interdependencies.
Microservices
The microservices pattern is an evolution of SOA, where the application is broken down into small, independent services, each focusing on a specific business capability.
These microservices should be independently deployable and scalable, allowing for greater flexibility and resilience.
This pattern can facilitate rapid development and continuous deployment, though it requires careful management of inter-service communication and dependencies.
Composable Services
The composable services pattern is an evolution of microservices and emphasizes building applications using modular, reusable services that can be dynamically composed and configured to create the desired functionality.
These services are designed to be highly interoperable, discoverable, and self-configuring — allowing for dynamic updates and scaling to support dynamic and resilient business and operational needs.
This pattern can promote agility and rapid innovation, as services can be mixed and matched to quickly create or adapt features, but it requires careful governance to ensure service compatibility and integration.
Each of these architectural patterns offers different strengths and trade-offs, and the choice of which to use depends on the specific goals, complexity, and scalability requirements of the system being developed.
An application system can be constructed from almost any combination of these patterns — though it is a too common practice to pick one and try to cram all application use cases into it — inadvertently increasing, rather than minimizing, complexity. Composable services avoid this issue.
Inter-Module Communication
In programming, different paradigms are used to handle the interaction and communication between various components or systems. Here’s a quick look at procedure calls, remote procedure calls, messaging, and event publishing:
Procedure Calls
A procedure call is the basic way to invoke a function or subroutine within the same program or linked module. It’s a direct call where the calling code executes the procedure and may get a return value upon completion.
How procedure calls work:
- A function or procedure is defined in the program.
- Another part of the program — or a linked program — calls this procedure by name, passing any required parameters.
- Control flow is transferred to the called procedure.
- Once the procedure finishes its execution, control is returned to the calling code, optionally with a return value.
Remote Procedure Calls (RPCs)
RPCs extend the concept of procedure calls to networked or distributed systems. An RPC allows a program to cause a procedure to execute in another machine or address space.
How RPCs work:
- The client program makes a procedure call as if it were local, but middleware implements it as a remote call.
- The call is sent over the network to a server.
- The server executes the procedure and returns the result back to the middleware.
- The client receives the result and continues its execution.
Messaging
Messaging refers to the synchronous and asynchronous communication between components or systems via messages. Each message is a self-contained data record sent from one component to another.
How Messaging works:
Components communicate by sending messages through a message broker, orchestrator, or queue.
- The sending component sends the message to the orchestrator or queue. Responses can be returned when the request is synchronous.
- The receiving component receives the message from the broker or orchestrator — or retrieves the message from the queue — and processes it.
- The sender and receiver can operate independently — synchronously or asynchronously — allowing for loose coupling and scalability.
Events
Components communicate by publishing events. An event represents a significant state change or required action that occurs within a system and is best implemented as a message. Other components that are interested in these events can subscribe to and react to them.
How Events work:
- A component (publisher) detects the state change or required action and publishes the event to a topic queue.
- Other components (subscribers) that are interested in that topic poll the topic queue, fetch the event, and react to it.
- This decouples the publisher from the subscribers, allowing them to evolve independently.
Summary
- Procedure Calls: Direct, synchronous function or subroutine invocation within the same program or linked module.
- Remote Procedure Calls (RPCs): Extend procedure calls over a network, allowing a function on one system to execute a function on another.
- Messaging: Synchronous and asynchronous communication where messages are exchanged between systems or components via a message broker, orchestrator, or queue.
- Events: A pattern where changes or actions (events) are broadcast, allowing subscribed components to react to those events independently.
Each of these paradigms serves different purposes and is suited to different architectural needs. More than one communication paradigm can be — and often should be — used within the same application system.
Programming Paradigms
A programming paradigm is a fundamental style or approach to programming that guides how developers structure and write code. It encompasses the principles, concepts, and practices used to solve problems within a programming language.
Different paradigms offer different ways to think about and organize programs, each with its strengths and weaknesses. Let’s examine a few of the most commonly used:
Procedural Programming
Procedural programming is a paradigm that organizes code into procedures or routines, which are sequences of instructions that operate on data.
The focus is on the sequence of actions to be performed, with programs typically structured using functions or procedures that manipulate data through a series of steps. This paradigm is straightforward and easy to understand for tasks that follow a clear step-by-step process.
- Popular Languages: C, Pascal, Fortran
Object-Oriented Programming (OOP)
Object-oriented programming is a paradigm centered around the concept of objects, which are instances of classes that encapsulate data and behavior. The focus is on modeling application entities as objects with attributes (data) and methods (functions).
OOP promotes principles like messaging, encapsulation, polymorphism, and inheritance which help in organizing and managing complex software by creating reusable and modular components.
- Popular Languages: Java, Python, C++
Functional Programming
Functional programming is a paradigm that treats computation as an evaluation of mathematical functions. It emphasizes immutability, statelessness, and the use of higher-order functions.
Programs are constructed by composing functions, avoiding changing-state and mutable data, which can result in more predictable and testable code. Functional programming encourages a declarative style, focusing on what to do rather than how to do it.
- Popular Languages: Haskell, Kotlin, Rust
Each of these paradigms offers a different approach to structuring and solving problems in software development, with procedural focusing on processes, object-oriented on objects, and functional on functions.
Wrapping Up
Architecture is important for managing complexity when building software — and doing software architecture well requires good communication among an application’s stakeholders and developers. It helps to be using the same language.
If you found this article useful, a clap would let us know that we’re on the right track.
Thanks!
Getting Architecture Right: Part 2 shows how we can use Lean Architecture with composable services to weave parts of multiple architectural and inter-module communication patterns into a coherent application system that is highly functional, performant, economical, and responsive to evolving customer needs and feedback.
Suggested Reading:
- Composable Services, describes what makes a service composable.
- Designing with Composable Services, using composable services to optimize application design tradeoffs.
- Building with Composable Services, provides a more formal and detailed description of the composable services architectural pattern.
- The Magic of Message Orchestration, introduces the wiring that connects composable components.
- Building Software Systems, breaking through the complexity of developing software.
- Designing a REST API, why messaging using state representations beats remote procedure calls.
- Why Architecture Matters — Part 1, why we should care.
- Why Architecture Matters — Part 2, more why we should care.