Introduction
In a previous article of this series, we explored the high-level design of the system, outlining its key components and interactions. Now, we shift our focus to the backend architecture, ensuring that we have a solid foundation before we start coding. A well-defined architecture serves as a battle plan, helping to avoid unnecessary complexity, improve maintainability, and facilitate future scalability.
In this article, we will explore the Onion Architecture and how it is applied within a modular monolith. By progressively drilling down from a high-level black box to detailed module structures, we ensure a clear separation of concerns and a scalable design.
All the diagram in the article are just an example. When designing a solution they should live and evolve. Since I want you to be able to reproduce this kind of documentation for your project I have added the PlantUML code of each diagram.
High-Level Black Box View
At the highest level, the system consists of two major components:
Frontend: A React-based web application that communicates with the backend.
Backend: A Spring Boot application handling all business logic and data persistence.
Diagram:
@startuml
package "System" {
[Frontend] --> [Backend]
}
@enduml
Backend Architecture Drill-Down
Black Box View
The backend follows a modular monolith approach, meaning it consists of multiple independent modules while sharing a common codebase. This design promotes maintainability and scalability without the complexity of microservices.
Backend Modules:
User Management (Handles authentication, profiles, and permissions)
Messaging (Manages chat and real-time communication)
Channel Management (Organizes conversations and groups)
Notification Service (Delivers push and in-app notifications)
Media Processing (Handles file uploads, storage, and playback)
Modules communicate via synchronous REST APIs and asynchronous messaging (Azure Event Hubs) where necessary.
Black Box Diagram: Modular Monolith with Modules
@startuml
scale 1.2
package "Backend (Modular Monolith)" {
package "User Management Module" {
}
package "Messaging Module" {
}
package "Channel Management Module" {
}
package "Notification Service Module" {
}
package "Media Processing Module" {
}
}
"User Management Module" -[hidden]d-> "Messaging Module"
"Channel Management Module" -[hidden]d-> "Messaging Module"
"User Management Module" -[-> "Messaging Module" : "REST API"]
"Messaging Module" -[-> "Notification Service Module" : "Event Hub"]
"Channel Management Module" -[-> "Messaging Module" : "REST API"]
"User Management Module" -[-> "Channel Management Module" : "REST API"]
"Media Processing Module" -[-> "Messaging Module" : "Event Hub"]
@enduml
Each module is responsible for a distinct business function, ensuring a clear separation of concerns.
White Box View
Within each module, we apply Onion Architecture, structuring the internals into concentric layers that enforce dependency rules.
Onion Architecture Layers:
Core Layer (Domain): Contains domain models and business rules.
Application Layer: Defines use cases and orchestrates business logic.
Infrastructure Layer: Implements repositories, external integrations, and APIs.
Each module follows this layered approach to maintain a clean architecture.
White Box Diagram: Internal Structure of Modules with Onion Architecture
@startuml
scale 1.2
package "User Management Module" {
package "Core Layer" {
class User
class Role
class Permission
}
package "Application Layer" {
class UserService
class AuthService
}
package "Infrastructure Layer" {
class UserRepository
class AuthProvider
}
}
package "Messaging Module" {
package "Core Layer" {
class Message
class Conversation
}
package "Application Layer" {
class MessagingService
}
package "Infrastructure Layer" {
class ChatRepository
}
}
@enduml
This white box view reveals the internal structure of each module, showing how classes interact within layers while adhering to the Onion Architecture principles.
Why Use Onion Architecture in a Modular Monolith?
Separation of Concerns: Business logic remains independent of frameworks.
Decoupled Dependencies: Core business logic does not depend on external services.
Explicit Boundaries: Communication between layers follows well-defined interfaces.
Maintainability: Changes in external technologies don’t impact the core.
Scalability: Modules can be extracted into microservices if needed later.
Testability: Domain logic can be tested in isolation without external dependencies.
This structure ensures that the backend remains well-organized, scalable, and adaptable while leveraging Azure technologies for storage, authentication, and messaging.
As said before these diagrams could be incomplete at the moment but they give an idea of what does it mean do decide the main software architecture and document it.
Let me know if you think I should go deeper, otherwise we will proceed with the next topics: some detail about the technology selection and the deployment view.
Articles I enjoyed this week
Organizational Skills Beat Algorithmic Wizardry by James Hague
Coding Challenge #84 - Mandelbrot Set Explorer by John Crickett
Nice introduction to Onion Architecture Riccardo!