Software Architectures: Styles and Structure Partitioning
Table of contents
- Software Architectures
- Software Architecture Structure Partitioning Implementation
- Architecture Style and Structure Partitioning Use Cases
- TODO System Example
- Software Architecture Diagrams
- Bonus: Hexagonal Architecture
Throughout my career as a software developer, while working for a big consultancy software company, I had the opportunity to be actively involved in multiple projects with different clients. One of the most fruitful experiences was when I had to deal with legacy systems and green field ones that were implemented with monolithic, layered, service-oriented, event-driven, microservices, and microkernel architectures. Such experience provided a mechanism for me to decide what type of architecture style and structure partitioning should be implemented, based on the project's scope: its business needs and client requirements.
In this post, I am going to share what software architecture style and structure partitioning are, when they should be implemented, use cases, and a TODO example system where we are going to put everything explained into practice.
The consultancy company where I worked stipulated two main defined practices or activities to be followed upon starting a project with a client, which were: discovery and inception. Those activities involved the participation of multiple team members with different roles like BA, XD, PM, QA, Dev, and Infrastructure engineers, with the goal of mapping all the requirements and perspectives from the business and tech side.
On every project that requires consultancy services, we usually get started with the discovery phase, whose objective is for the project team to gather and evaluate information about the project requirements, stakeholders, and constraints. Next was to run the Inception phase where we focused on defining the project scope usually with a minimum viable product (MVP), establishing a high-level project plan, and designing the initial software architecture.
During the inception phase, the technical team was more actively engaged as one of the key outcomes was architecture design, the foundational software components, and interactions within a software system at a high level, which serves as a blueprint for its development. This strategic decision-making process entails the consideration of the architecture components and guides the development team in implementing functional and non-functional requirements that enable organizations to adapt to ever-evolving business needs.
At that time I used the term software architecture structure to provide a high-level architecture to the clients and team members, then during the development phase I used the term software architecture patterns to implement the software component. After reading Richards Mark's book: Software Architecture Patterns, I found that there is a better way that could be used to differentiate those phases related to the architecture design and implementation which are: architecture style and architecture structure partitioning. Those terms will be used on the following topics of this post to describe the steps to follow in the process of software architecture design and implementation.
A software architecture style embodies a collection of design principles, guidelines, and best practices for organizing and structuring software system components at a high level. The styles provide a consistent approach to solving common software design problems that often encapsulate proven solutions, making it easier for developers to create maintainable and scalable systems.
In the field of software development, it is a well-recognized fact that multiple solutions often exist for a given problem or use case. This wide diversity in problem-solving approaches has led to the popular use of the phrase "it depends" within the industry. The phrase is popular in consultancy companies since it highlights the importance of considering different factors, such as requirements, constraints, and context when determining the most suitable solution for a specific software challenge.
In order to select the suitable software architecture style for the project we should align it with both the business needs (goals and objectives of the organization) and client requirements (specific functional and non-functional requirements). Additionally, performing a trade-off analysis can explain objectively why a specific option is the best suitable architecture style for the project. This phase of the software architecture design process has significant importance, as the subsequent implementation phase will be considerably impacted by the decisions made during this stage so careful planning and analysis are essential for a successful project.
Some examples of common architecture styles are:
Service Oriented Architecture (SOA)
Event-Driven Architecture (EDA)
Architecture Structure Partitioning
Once we have chosen an architecture style, we can move on to software architecture structure partitioning. This process helps create a modular, maintainable, and organized system that aligns with the chosen architectural style. The choice of partitioning strategy depends on factors such as the system size, complexity, and specific architecture requirements.
Architecture structure partitioning is related to the development phase or implementation of the project with the goal of delivering what was agreed upon as an MVP. In the next topics, we are going to cover the implementation aspects of software structure partitioning.
Software Architecture Structure Partitioning Implementation
In software engineering, developing a well-structured and maintainable system is crucial for long-term success. One of the key approaches to achieve this is through software architecture structure partitioning. The implementation process for structure partitioning involves dividing a system into smaller, more manageable units, which improves modularity, and maintainability. It can be approached in two primary ways: technical and domain.
Structure Partitioning: Technical
This approach focuses on organizing the system into distinct units according to their technical responsibilities, which can promote the separation of concerns and reduce complexity. Examples of technical partitioning strategies include:
Layered:Organizing the system into distinct horizontal layers, where each layer provides services to the layer above it and depends on services from the layer below it. Common layers include presentation, business, and persistence.
Service-Oriented Architecture:Decomposing the system into independent services that communicate with each other via well-defined interfaces or APIs.
Event-Driven Architecture:The components are organized around the asynchronous communication of events. Event producers generate events, while event consumers process them. The event bus or message broker serves as the communication backbone, connecting producers and consumers while maintaining their decoupling.
Microkernel:Also known as the plugin architecture, organizes components into a core system (microkernel) and a set of plugins or extensions. The microkernel is responsible for providing essential system functionality and managing communication between plugins, while the plugins implement specific features, business logic, or domain functionality. This is a particular architecture since it could be either technical or domain structure partitioning, it will depend on how the plugins are used. In the case of technical structure partitioning, the plugin components are designed to address specific technical aspects or concerns. For example, if the plugins represent different modules in an analytics platform, such as data connectors, data transformations, and export formats the architecture can be considered technically partitioned.
Opting for technical structure partitioning is recommended when a project demands a clear separation of concerns, such as user interface, business logic, and data access, or if you are in a small team that can manage all the system components. For example, a multi-cloud management platform can leverage technical partitioning to support integration with multiple cloud providers' APIs, allowing users to manage resources across different cloud environments using a single interface. Similarly, a workflow automation system can benefit from technical structure partitioning by supporting multiple workflow engines and scripting languages, providing flexibility for users to create and execute custom workflows tailored to their specific needs.
Structure Partitioning: Domain
This approach aligns the software structure with the underlying domain model, making it easier to understand and reason on. Domain partitioning promotes a close relationship between the system's organization and the real-world problem it aims to solve. Examples of domain partitioning strategies include:
Microkernel:As explained earlier microkernel architecture can be considered a combination of both technical and domain structure partitioning due to the way it organizes its components. From a domain structure partitioning perspective, it enables the organization of code by business areas. Each plugin or module can represent a specific domain or business functionality, which can be developed, tested, and maintained independently of the others. For example, if the plugins represent different modules in a learning management system (LMS), such as student progress tracking, online assessment, and learning analytics, the architecture can be considered a domain partition.
Modular Monolith:It organizes components into distinct, self-contained modules within a single codebase, with each module representing a specific functional area or domain. The components are designed to minimize dependencies and coupling between modules, leading to a more maintainable, scalable, and understandable system.
Microservices:It arranges elements into compact, self-governing units centered on a particular business area or capability. Each microservice is responsible for its own data, logic, and processing, allowing for independent development, deployment, and scaling.
Domain structure partitioning is a suitable choice when a project centers on modeling complex business domains, and you want to align the software structure with the underlying domain concepts and entities or when you are working on a large-scale project with multiple teams, where each team is responsible for a specific area of the business domain. For example, a health management system can benefit from domain partitioning by dividing its components into distinct modules, such as patient records, appointment scheduling, and billing, allowing developers to focus on the specific functionality of each module without affecting others. Another example could be, an inventory management system that can take advantage of domain partitioning by separating modules responsible for product catalog management and order processing, enabling the system to be easily extended and adapted to accommodate new business requirements or changes in existing processes.
Architecture Style and Structure Partitioning Use Cases
Software architecture styles and structure partitioning techniques can be applied in different use cases to address specific system requirements. In this section, we are going to share some use cases where it can be applied. As a disclaimer, the architectures selected for the use cases represent one way to implement the software system, there are multiple ways to do it, so we recommend always taking into account the project scope (business needs and client requirements) to make these architecture decisions:
E-Commerce platform: This platform type usually offers services such as product listing, inventory management, shopping cart management, order processing, and user account management. The layered architecture style could be suitable for an e-commerce platform as it promotes a clean separation of concerns by organizing code into distinct layers: presentation, business logic, and data access. In this use case taking into account the layered architecture style selected, the modular monolith architecture (domain structure partitioning) could be used to divide the application into well-defined, decoupled modules that focus on specific business capabilities. For example, the platform can be organized into modules for product catalog, inventory, shopping cart, order processing, and user management. Each module can be developed and maintained independently while still being part of a single, cohesive system.
Travel booking platform: A travel booking platform typically requires integration with multiple external systems, such as airline reservation systems, hotel booking systems, car rental services, and payment gateways. A service-oriented architecture (SOA) style is well-suited for this use case, as it organizes components as reusable, interoperable services that communicate through standard interfaces. A microservices architecture (domain structure partitioning) can be used to manage the diverse functionalities described in the integration with external systems. This approach organizes components into small, autonomous services focused on specific business capabilities. Each microservice is responsible for its own data, logic, and processing, allowing for independent development, deployment, and scaling.
Notifications platform: A notifications platform is typically characterized by the need to process and react to different asynchronous events, such as user actions, system state changes, or external triggers. An Event-Driven Architecture is well-suited, as it allows for asynchronous communication between components, which can handle the high volume of events and notifications efficiently. A microservices architecture (domain structure partitioning) could be used to manage the multiple functionalities in a notifications service, such as message routing, user management, message delivery, and analytics.
Content management system: A content management system (CMS) could use a microkernel or plugin's architecture style to enable a straightforward extension of the system with new features like a custom theme, content editor, content search, user management, analytics, and so on. Then on the kernel and plugin modules, modular monolith (domain structure partitioning) can be used to organize components into well-defined modules.
As you may note there are multiple combinations when selecting architecture style and structure partitioning. The choice is not strictly sequential, as both aspects are essential in the design of a software system. However, it can be helpful to consider the architecture style first, as it provides a high-level view of the system's organization, communication, and interaction patterns. Then, the architecture structure partitioning can be considered to further refine the organization of components or services within the system.
TODO System Example
In this example, we will design a simple TODO system using a suitable software architecture style and structure partitioning strategy.
To develop the TODO system, these are the requirements we have to fulfill:
It will provide a UI that will be rendered on the browser with a responsive design suitable for both desktop and mobile devices
It will allow the following operations: list, create, edit, and remove.
The TODO status will be: todo, in-progress, block, and done.
The information will be saved in a relational database.
The core components will contain automated tests that will run in a pipeline every time a change is made to the repository.
Software Architecture Style Selection
In this section, we are going to analyze the options available and what is the most suitable for the TODO system taking into account the client's requirements.
Service-Oriented Architecture (SOA)
SOA typically involves the creation and management of multiple services, each exposing a specific set of functionalities via well-defined interfaces. In contrast, a TODO system is relatively simple, consisting of basic CRUD operations (Create, Read, Update, Delete) on TODO items. Implementing SOA for such a system can introduce unnecessary complexity and overhead. Also implementing an SOA-based system typically requires more development effort and expertise since it’s commonly used for larger, distributed systems with multiple, autonomous services. This architecture style adoption could increase the time and cost of developing and maintaining the TODO system.
The core strength of EDA lies in its asynchronous, non-blocking communication. However, a TODO system primarily involves basic CRUD operations, which can be efficiently handled using synchronous, request-response communication patterns, also, the processing of events can introduce latency due to the time taken for events to be published, propagated, and consumed. The asynchronous nature of EDA might not provide significant benefits for the TODO system.
One of the main benefits of microkernel architecture is its extensibility, allowing for the addition of new features or functionalities through plugins without modifying the core system. However, a TODO system's scope is generally limited, and the requirement for extensive extensibility may need to be more significant to justify adopting a microkernel architecture.
Microservices involve the deployment and management of multiple independent services, each with its own infrastructure requirements. While this can provide benefits such as improved scalability and fault tolerance, it also increases infrastructure resource consumption for a relatively simple application like a TODO system. Also in a microservices-based system, services communicate with each other over a network, typically using protocols such as REST or gRPC. This inter-service communication can introduce latency, potential network bottlenecks, and additional complexity in terms of managing communication patterns and data consistency.
Modular monolithic architecture maintains the simplicity of a single application, making it easier to understand, develop, and maintain. Also, this architecture style emphasizes the clear separation of concerns and modularization of components within the application, allowing for better organization, maintainability, and extensibility. This approach can help manage the complexity of the TODO system while still keeping it easy to understand and modify.
Layered architecture promotes a clear separation of concerns, making it easier to understand, develop, and maintain the system. Each layer focuses on a specific aspect, such as presentation, business logic, or data access, enabling better organization and modularization. With a clear separation between layers, it becomes easier to modify or update individual components without affecting the entire system. This improves maintainability and allows for more straightforward adaptations or enhancements in the future.
When comparing the architectures styles such as Layered, SOA, EDA, Microkernel, and Microservices, Layered architecture provides a simpler and more straightforward approach that aligns well with the requirements of a basic TODO system (UI, backend, and database). While SOA, EDA, and Microservices are more suitable for complex, distributed, and highly scalable systems, they introduce additional complexity and overhead that may not be necessary for a simple TODO system. Microkernel architecture, on the other hand, is more relevant for systems with core functionality that can be extended through plugins, which is not a primary requirement for the TODO system. Comparing Layered architecture with Modular Monolithic architecture, both approaches offer modularity, maintainability, and a clear organization. However, layered architecture provides a more explicit separation of concerns, dividing the system into distinct layers responsible for the UI, backend service, and database interactions. This separation simplifies development, testing, and maintenance, making it easier to evolve the system over time. As a result, layered architecture is a more suitable choice for a TODO system, as it strikes the right balance between simplicity, maintainability, and ease of development, while still delivering the required functionality.
Software Architecture Structure Partitioning Selection
When considering a TODO system that requires a UI communicating with a backend service interacting with a database and having selected the Layered architecture style, it is essential to evaluate the available architecture structure partitioning options. As we explained in the previous topics, there are two primary structure partitioning types: technical and domain partitioning. Technical partitioning focuses on separating code based on functionality, while domain partitioning emphasizes dividing code by business areas or features. Comparing technical partitioning with domain partitioning in the context of layered architecture style, technical partitioning aligns well with the inherent separation of concerns provided by the layered approach. In the case of the TODO system, technical layered partitioning could ensure a clear separation of the UI, backend service, and database interaction layers, which simplifies development, testing, and maintenance. Domain partitioning architectures options, while useful for larger systems with multiple business areas or features, may not be practical for a simple TODO system. Based on the previous analysis, we will use the Layered structure partitioning for the TODO system implementation.
With the style and structure partitioning selected the system will be composed of the following layers:
This layer will be responsible for rendering the user interface (UI) in the browser, handling user interactions, and presenting the data to the user.
This layer will contain the business logic for the TODO system, handling user requests and coordinating the interactions between the Presentation and Persistence layers.
Technologies: A back-end framework like Express.js (Node.js), Flask (Python), or Spring Boot (Java) can be used to implement the business layer.
This layer will be responsible for communicating with the PostgreSQL relational database to store, retrieve, and manage TODO data.
Technologies: An Object-Relational Mapping (ORM) library like Sequelize (Node.js), SQLAlchemy (Python), or Hibernate (Java) can be used to interact with the PostgreSQL database.
The features of the TODO system will be implemented in the following way:
List: The Business layer retrieves the list of TODOs from the Persistence layer and sends the data to the Presentation layer for display.
Create: The Presentation layer captures user input and sends a request to the Business layer, which validates the input and calls the Persistence layer to store the new TODO in the PostgreSQL database.
Edit: The Presentation layer sends an update request to the Business layer with the modified TODO data, which then updates the corresponding record in the database using the Persistence layer.
Remove: The Presentation layer sends a delete request to the Business layer, which then removes the corresponding record from the database using the Persistence layer.
By selecting the Layered architecture style and Technical structure partitioning for the TODO system, we have created a modular, maintainable, and well-organized design that can be easily extended or modified to accommodate new features or requirements.
You can find the code implementation example in the following links:
Nextjs TODO Web App: https://github.com/herrera-luis/layered-next-todo-service
Flask TODO API: https://github.com/herrera-luis/layered-flask-todo-service
Software Architecture Diagrams
When the time to diagram software architecture components in detail comes, multiple approaches are available for creating detailed diagrams, which offer their own benefits and advantages. These methods aim to capture different aspects of the system, such as components, relationships, and interactions, to facilitate understanding and communication among stakeholders. Some popular diagramming techniques include the Unified Modeling Language (UML), the C4 model, flowcharts, and data flow diagrams.
In my experience, the C4 model is popular and highly used by software architects to diagram in detail software architectures since it incorporates multiple abstraction levels, including level 1 - system context, level 2 - container, level 3 - component, and level 4 - code. Most of the clients usually make the diagrams until level 3 which is the components diagram since it is good for long-live documentation and the audience are architects and developers. Level 4 is considered optional, as it provides short-lived documentation that can be auto-generated by integrated development environments (IDEs).
For our TODO system, we are going to create C4 diagrams but just until level 3 since the code is not ready for production and should be accommodated or just used as a reference example which means level 4 is highly susceptible to changes.
Level 1: System Context Diagram
The Level 1 System Context Diagram in the C4 model provides a high-level view of the entire system, illustrating its interactions with customers and external systems. In our TODO system, we are not integrating external systems like authentication or notification methods so our primary context at this level will be system and customer.
System: It represents the entire TODO system as a single entity, encapsulating all its components and functionalities.
Customer: It represents the user that interacts with the TODO system.
Level 2: Container Diagram
This level focuses on the different containers within the system, showcasing their responsibilities and how they interact with each other. Our TODO system at this level will be composed of the following containers:
Web App (User Interface): Include the web application that serves as the customer interface for interacting with the API app. This container manages customer input, displays tasks, and communicates with the backend services.
App API: This container is responsible for processing customer requests, managing tasks, and interacting with the data storage layer.
Database: Data storage system is used to store the TODO data
Level 3: Components Diagram
In the TODO system, the Level 3 Components Diagram focuses on the internal structure and interactions of the main components within the system, so it is composed of the following parts.
Web App (UI) :
This container manages the presentation layer of the application, handling customer input and displaying the app's data, such as tasks, due dates, and completion status, it has the following components.
Page compoment: Responsible for composing the layout and structure of a specific page, assembling the necessary components, and handling any page-specific logic.
Components: Implement reusable UI elements and handle associated logic, such as user input, data display, and interactions. Examples: ConfirmationModal.tsx, ErrorBoundary.tsx, TodoForm.tsx, TodoItem.tsx, TodoList.tsx.
Contexts component: Oversee application state management, handle business logic, and provide a global state management solution, enabling efficient data flow and state sharing across the application.
Services component: Facilitate communication with external data sources, such as APIs or databases, and execute CRUD operations. These components encapsulate data retrieval, creation, update, and deletion logic, isolating it from the rest of the application.
In this container, the API endpoints handle incoming HTTP requests and provide RESTful services for managing todo items, it’s composed of the following parts:
Routes Component: This component manages the API routes of the API app, providing an interface for client applications to interact with the system. It defines the HTTP methods (e.g., GET, POST, PUT, DELETE) and the corresponding URLs for different operations, such as creating, updating, deleting, or fetching TODOs. The Routes Component handles incoming requests, directing them to the service components within the application for further processing. It also ensures proper responses are sent back to the client, containing necessary data or status codes.
Service Component: Responsible for the core functionality of the API app, this component manages TODO creation, modification, deletion, and completion. It also handles the processing of any business logic or rules associated with tasks.
Migrations Component: This component is in charge of handling the database schema changes and updates for the API app. It maintains a version history of the database schema, enabling smooth transitions between different versions as the application evolves. By using the Migrations Component, developers can automate the process to apply schema updates, ensuring that the database remains in sync with the application's requirements.
Database Component: Responsible for persisting the TODOs and their associated data, ensuring that information is stored and retrieved from a data source (e.g., a database, or in-memory storage like sqlite) as needed.
Bonus: Hexagonal Architecture
Hexagonal architecture is also known as ports and adapters, in the previous topic I didn’t present it as an architecture style as I consider it as an architecture pattern with a set of principles that focuses on a specific aspect of software design: the separation of concerns and decoupling of components. So as a pattern, it can be applied to different architecture styles. Let me present you some examples of combining Hexagonal Architecture with different architecture styles:
Layered Architecture organizes components into distinct layers, such as presentation, business, and data access. Applying Hexagonal Architecture principles, you can enhance the separation of concerns by introducing ports and adapters to manage dependencies between layers. The core business logic remains in the domain layer, while the application layer defines the ports needed for various interactions. Presentation and data access layers can be considered adapters that implement the defined ports.
In a Microservices Architecture, components are organized into small, autonomous services focused on specific business domains. Hexagonal Architecture can be applied to each microservice individually, keeping the core domain logic separated from external dependencies through ports and adapters. This can enhance the maintainability and adaptability of individual microservices, making it easier to change or replace specific parts of the system. For instance, when creating a microservice for processing payments, the core business logic handles the payment processing rules, while adapters handle interactions with external systems like payment gateways and user notifications.
EDA focuses on the asynchronous communication of events between components. Hexagonal Architecture can be applied to separate the core business logic from event processing components and external systems. Event publishers, subscribers, and event handlers can be implemented as adapters that interact with the core through the defined ports. This allows for better decoupling of components and more flexibility in handling events.
A Modular Monolith is a monolithic system organized into modules with clear boundaries and separation of concerns. Applying Hexagonal Architecture can further enhance modularity by isolating each module's core domain logic and using ports and adapters to manage dependencies between modules and external systems. For instance, in an e-commerce application, modules such as product management, customer management, and order processing can be designed using Hexagonal Architecture, with adapters for communication between modules and external dependencies like databases or third-party APIs.
The microkernel architecture organizes components into a core system (microkernel) and a set of plugins or extensions. Combining Microkernel Architecture with Hexagonal Architecture allows for a clear separation of core functionality and domain-specific features. In this approach, the microkernel manages the central functionality, and plugins or extensions implement additional features using Hexagonal Architecture. For example, in the CMS, the microkernel could handle basic content storage and retrieval, while plugins for different content formats such as images, videos, and documents, are developed using Hexagonal Architecture. Each plugin has input and output ports for communication with the microkernel and external systems, and adapters translate data or requests between the microkernel, external systems, and plugins.
This post offers a high-level overview to determine -based on the context- the proper architecture style and structure partitioning. We presented different use case examples with limited context to illustrate the suitability of specific architecture styles and structure partitioning. Additionally, a code example of a TODO system was showcased in order to demonstrate the design, implementation, and diagrams using the C4 model. I hope this post provides some insights when facing the challenge of selecting a software architecture for the system you are developing.