Overview
Layered Architecture separates software into four conceptual layers: Interface, Application, Domain, and Infrastructure. Each layer has a specific responsibility and clear dependency rules. jMolecules provides annotations that help you mark classes or packages as belonging to one of these layers, ensuring the right abstractions, boundaries, and dependencies are in place.
In a typical layered approach:
- Interface Layer handles external communication (user interfaces, REST endpoints, CLI, etc.).
- Application Layer coordinates business processes and high-level flow.
- Domain Layer contains the core business rules, entities, value objects, and aggregates.
- Infrastructure Layer provides technical capabilities such as persistence, messaging, or external service integrations.
This document describes the “must depend on” and “must not depend on” rules between these layers and illustrates their usage with a simple example.
Dependency Rules: Must Depend On and Must Not Depend On
1. Must Depend On
- Definition: Specifies which layers a given layer is allowed to rely on.
- Purpose: Enforces a logical flow from higher-level layers (like Interface) down to the Domain or to cross-cutting Infrastructure components where appropriate.
2. Must Not Depend On
- Definition: Specifies which layers a given layer cannot rely on.
- Purpose: Prevents circular dependencies and keeps each layer focused on its role, thus preserving a clean architecture.
jMolecules Annotations and Their Constraints
The table below summarizes the allowed and restricted dependencies for each jMolecules layer annotation:
Annotation | Must Depend On | Must Not Depend On | Represents |
---|---|---|---|
@InterfaceLayer | @ApplicationLayer | @DomainLayer, @InfrastructureLayer (directly) | External API / UI Handles incoming requests (web, CLI, etc.) |
@ApplicationLayer | @DomainLayer | @InterfaceLayer, @InfrastructureLayer (directly)* | Business Orchestration Coordinates flows between Domain and other subsystems |
@DomainLayer | None | @InterfaceLayer, @ApplicationLayer, @InfrastructureLayer | Core Business Logic Entities, Value Objects, Aggregates, Domain Services |
@InfrastructureLayer | @DomainLayer, @ApplicationLayer (if needed) | @InterfaceLayer | Technical Services Persistence, Messaging, external integrations |
* In many implementations, the Application Layer may indirectly use Infrastructure via interfaces or repositories, but it should not directly invoke infrastructure details. Usually, the Infrastructure Layer provides implementations of interfaces that the Application Layer consumes.
Example: User Registration System
Let’s illustrate these rules with a simple “User Registration” scenario.
- Interface Layer (
@InterfaceLayer
):- Provides controllers or other interfaces to handle external requests (e.g., REST calls).
- Application Layer (
@ApplicationLayer
):- Orchestrates the registration flow, coordinating with the Domain and Infrastructure (through interfaces) as needed.
- Domain Layer (
@DomainLayer
):- Defines the
User
entity, domain services, and core business rules.
- Defines the
- Infrastructure Layer (
@InfrastructureLayer
):- Implements persistence for the
User
entity (e.g., via Spring Data JPA or another persistence framework).
- Implements persistence for the
Code Example
Domain Layer (User Entity)
@DomainLayer
data class User(
val id: String? = null,
val name: String,
val email: String
)
Application Layer (UserService)
@ApplicationLayer
class UserService(private val userRepository: UserRepository) {
fun registerUser(name: String, email: String): String {
// Business logic orchestration
val user = User(name = name, email = email)
// Could have domain logic or validations here
return userRepository.save(user)
}
}
Interface Layer (UserController)
@InterfaceLayer
@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {
@PostMapping
fun registerUser(@RequestBody userDto: UserDto): ResponseEntity<String> {
val userId = userService.registerUser(userDto.name, userDto.email)
return ResponseEntity.ok(userId)
}
}
data class UserDto(val name: String, val email: String)
Infrastructure Layer (UserRepository + Persistence)
@InfrastructureLayer
@Repository
interface CrudUserRepository : CrudRepository<User, String>
@InfrastructureLayer
class UserRepositoryImpl(private val crudUserRepository: CrudUserRepository) : UserRepository {
override fun save(user: User): String {
return crudUserRepository.save(user).id!!
}
override fun findById(id: String): User? {
return crudUserRepository.findById(id).orElse(null)
}
}
// The Application Layer references this interface, but not the direct CrudRepository
interface UserRepository {
fun save(user: User): String
fun findById(id: String): User?
}
Dependency Rules in Action
-
UserController (
@InterfaceLayer
):- Must Depend On:
UserService
(@ApplicationLayer
). - Must Not Depend On:
UserRepositoryImpl
(@InfrastructureLayer
) orUser
(@DomainLayer
) internals directly for business logic. - It can, however, pass domain data (like
UserDto
) to the Application Layer.
- Must Depend On:
-
UserService (
@ApplicationLayer
):- Must Depend On:
UserRepository
(@InfrastructureLayer
interface) only through well-defined boundaries or domain objects. - Must Not Depend On:
UserController
(@InterfaceLayer
) or framework-specific classes from Infrastructure directly.
- Must Depend On:
-
User + any other model classes (
@DomainLayer
):- Must Not Depend On:
UserService
orUserController
. - They hold the core business logic or data.
- Must Not Depend On:
-
UserRepositoryImpl (
@InfrastructureLayer
):- Must Depend On: Domain classes (e.g.,
User
) if needed to map to the database. - Must Not Depend On:
UserController
(@InterfaceLayer
) or application classes that orchestrate domain logic.
- Must Depend On: Domain classes (e.g.,
Why These Rules Matter
-
Separation of Concerns
Each layer has a clear role, so changes in one layer (e.g., UI or persistence) don’t cascade throughout the system. -
Testability
Domain and Application layers can be tested in isolation by mocking Infrastructure or Interface layers. -
Maintainability
Well-defined boundaries prevent accidental coupling and make the codebase easier to comprehend and evolve. -
Technology Independence
You can swap technologies (e.g., switch from JPA to MongoDB) by only changing the Infrastructure layer, without affecting Domain or Application logic.
Conclusion
Using the jMolecules annotations for Layered Architecture ensures each layer remains focused on its core responsibilities:
- The Interface Layer handles external communication.
- The Application Layer orchestrates business flows.
- The Domain Layer holds the core business logic and rules.
- The Infrastructure Layer provides technical capabilities such as persistence, messaging, or external system integrations.
By adhering to the “must depend on” and “must not depend on” constraints, you achieve a system that is modular, testable, and more robust against changing technical requirements.