Spring: Thoughts on Backend Architecture
Contents
Notice
This text was originally translated using AI tools.
Introduction#
I have contemplated backend architecture for a long time.
Initially, I started by learning a simple Layered Architecture, but as the system grew, I identified issues with high coupling.
To resolve this, I introduced a multi-module structure. Recently, I have been studying Clean Architecture for domain-driven development and have begun applying it to new systems.
Below is the architecture I configured based on what I've learned.
Designed Architectural Structure#
src
├── core # Common domain logic and global configurations
├── service # Individual business service areas
│ ├── api # Presentation: Receiving requests and returning responses
│ ├── api.scheduler # Entry Point: Entry point for scheduling-based tasks
│ ├── application # Application: Usecase flow control and transaction management
│ ├── domain # Domain: Core business objects (Entities) and policy definitions
│ ├── infrastructure.* # Infrastructure: Technical implementations for DB, Cache, Storage, etc.
│ └── root # Boot: Dependency Injection (DI) and application runtime configuration
└── util # Technical auxiliary tools (Common, Encryption, etc.)- api
- The layer that receives client requests and returns processing results, responsible for mapping Controllers and Request/Response DTOs.
- application
- The layer that materializes the business flow of defined domain objects, manages transactions, and coordinates interactions with external systems.
- domain
- Defines core business rules and entities. Written in pure Java code without depending on external frameworks or libraries, ensuring high testability and independence.
- infrastructure
- Implements interfaces defined in the domain layer using actual technologies (JPA, Redis, etc.). It is isolated so that business logic remains unaffected even if the underlying technology changes.
- root
- Responsible for assembling each layer into a single, executable application.
Reflections on Application#
The most significant changes I felt while applying a domain-centric architecture to an actual system were the isolation of business logic and ease of testing.
- Pure Java-based Unit Testing: Since the domain layer does not depend on specific frameworks or external libraries, I could perform unit tests to verify core logic quickly and precisely through mocking without complex configurations.
- Efficient Infrastructure Verification: In the infrastructure layer, the separation of interfaces and implementations made it very easy to build test environments using Testcontainers. This allowed for safe verification of interactions without connecting to actual external systems.
Identifying the Problem#
While operating the actual system based on this architecture, several structural problems were identified when attempting to expand with new services. The problematic areas were as follows:
├── service
│ ├── application # Implementation of Business Services defined in Domain
│ └── domain # Collection of core business logic specifications (Interfaces)
│ └── business
│ └── user
│ ├── repository
│ └── service # The problematic point: Abstraction of Application usecasesThe issue was a lack of understanding of domain-driven design, leading to the placement of abstract classes for the application layer within the domain layer.
When I tried to develop a new service using the same domain, I was forced to share business logic specifications of existing services that were completely unrelated.
Improvement Direction: Separating Domain and Usecases#
I felt it was important to first accurately re-identify the definition of a Domain.
I had vaguely thought, "Specifications (Interfaces) do not change, so they are part of the domain," but I was able to clarify the boundaries through DDD (Domain-Driven Design).
Redefining Core Logic
- Domain Rules (Domain): Constraints and the lifecycle of the data itself. (e.g., "Files can only be extracted when the task is in a completed state.")
- Usecases (Application): Business procedures and flows. (e.g., "Find the file, compress it, convert it to a byte array, and provide it as a download.")
Based on this criteria, I moved the usecase-oriented services that existed in the domain layer to the application layer.
Redesigned Multi-Module Structure
I reorganized the structure to share common models while keeping the execution logic of each service strictly isolated.
src
├── core # System-wide common module
├── service.core # [Shared] Core area shared by all services
│ ├── domain # Pure entities and core policies
│ └── infrastructure.* # Common infrastructure implementations (DB, Cache, etc.)
├── service.web # [Module] Existing Web API service
│ ├── api # Web Controllers
│ ├── application # Usecases and interfaces dedicated to the Web service
│ └── root # Web Boot Configuration
├── service.work # [Module] New Backend Worker service
│ ├── application # Business logic dedicated to the Worker (Batch, processing, etc.)
│ └── root # Worker Boot Configuration
└── util # Common utilitiesConclusion#
The expected benefits of this redesign are as follows:
- Prevention of Knowledge Contamination: The
service.workmodule no longer needs to know about business specifications specific only toservice.web - Increased Cohesion: Since each service module possesses only the application usecases it requires, changes in one module have very little impact on others.
- Flexible Expansion: Even if new types of services are added in the future,
independent modules can be quickly attached based on
service.core.
Fortunately, the layers of the existing system were already somewhat well-separated, so there were no major issues during the architectural redesign.
Identifying and solving problems in unexpected areas has deepened my understanding of architecture.
While the current form may not be perfect, I hope my perspective on architecture becomes more flexible through this continuous improvement process.