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:
Each layer has strict rules about which other layers it can and cannot depend on. These rules prevent circular dependencies and keep each layer focused on its role.
Specifies which layers a given layer is allowed to rely on. Enforces a logical flow from higher-level layers (like Interface) down to the Domain or to cross-cutting Infrastructure components where appropriate.
Specifies which layers a given layer cannot rely on. Prevents circular dependencies and preserves a clean architecture.
| Annotation | Must Depend On | Must Not Depend On | Represents |
|---|---|---|---|
@InterfaceLayer |
@ApplicationLayer | @DomainLayer, @InfrastructureLayer | External API / UI — handles incoming requests (web, CLI, etc.) |
@ApplicationLayer |
@DomainLayer | @InterfaceLayer, @InfrastructureLayer* | 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 | @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.
Let's illustrate these rules with a simple "User Registration" scenario:
User entity and core business rulesUser via Spring Data JPA@DomainLayer
data class User(
val id: String? = null,
val name: String,
val email: String
)
@ApplicationLayer
class UserService(private val userRepository: UserRepository) {
fun registerUser(name: String, email: String): String {
// Business logic orchestration
val user = User(name = name, email = email)
return userRepository.save(user)
}
}
@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)
// The Application Layer references this interface, but not the direct CrudRepository
interface UserRepository {
fun save(user: User): String
fun findById(id: String): User?
}
@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)
}
}
@InterfaceLayer)UserService (@ApplicationLayer)UserRepositoryImpl (@InfrastructureLayer) or User (@DomainLayer) internals directly for business logicUserDto) to the Application Layer@ApplicationLayer)UserRepository (interface) and domain objectsUserController (@InterfaceLayer) or framework-specific classes from Infrastructure directly@DomainLayer)UserService or UserController@InfrastructureLayer)User) if needed to map to the databaseUserController (@InterfaceLayer) or application classes that orchestrate domain logic
Each layer has a clear role. Changes in one layer (e.g., UI or persistence) don't cascade throughout the system.
Domain and Application layers can be tested in isolation by mocking Infrastructure or Interface layers.
Well-defined boundaries prevent accidental coupling and make the codebase easier to comprehend and evolve.
Swap technologies (e.g., JPA to MongoDB) by only changing the Infrastructure layer, without affecting Domain or Application logic.
Using the jMolecules annotations for Layered Architecture ensures each layer remains focused on its core responsibilities:
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.