The jMolecules annotations help enforce Hexagonal Architecture (Ports & Adapters) principles, ensuring modularity, separation of concerns, and technology independence.
In Hexagonal Architecture, the application core communicates with the outside world exclusively through Ports (interfaces). Adapters implement these ports, connecting the core to specific technologies.
Dependencies always point inward. Adapters depend on Ports. The Application core depends on nothing external.
Specifies which components a given component is allowed to rely on. Ensures proper communication through defined interfaces.
Specifies which components a given component cannot rely on. Prevents tight coupling and enforces architectural boundaries.
| Annotation | Must Depend On | Must Not Depend On | Represents |
|---|---|---|---|
@PrimaryAdapter |
@PrimaryPort | @SecondaryPort, @Adapter | Technology-specific implementation driving the application via Port |
@PrimaryPort |
None | @Adapter, @SecondaryPort | Core interface for external systems to interact with the application |
@Application |
@Port | @Adapter | Core business logic, independent of technology or framework |
@SecondaryPort |
None | Technology-specific code | Core interface for application-driven communication with external systems |
@SecondaryAdapter |
@SecondaryPort | @PrimaryPort, @Adapter | Technology-specific implementation for external systems driven by Port |
@PrimaryPort
interface UserService {
fun registerUser(user: User): String
}
@PrimaryAdapter
@RestController
class HttpController(private val userService: UserService) {
@PostMapping("/users")
fun register(@RequestBody user: User): ResponseEntity<String> {
val userId = userService.registerUser(user)
return ResponseEntity.ok(userId)
}
}
@Application
class UserServiceImpl(
private val userRepository: UserRepository
) : UserService {
override fun registerUser(user: User): String {
// Business logic: Validate, process, and save user
return userRepository.save(user)
}
}
@SecondaryPort
interface UserRepository {
fun save(user: User): String
fun findById(userId: String): User?
}
@SecondaryAdapter
@Repository
class JpaUserRepository(
private val jpaRepository: CrudRepository<User, String>
) : UserRepository {
override fun save(user: User): String {
return jpaRepository.save(user).id
}
override fun findById(userId: String): User? {
return jpaRepository.findById(userId).orElse(null)
}
}
@PrimaryAdapter)UserService (@PrimaryPort)UserRepository (@SecondaryPort) or JpaUserRepository (@SecondaryAdapter)@Application)UserRepository (@SecondaryPort)JpaUserRepository (@SecondaryAdapter)@SecondaryAdapter)UserRepository (@SecondaryPort)UserService (@PrimaryPort) or HttpController (@PrimaryAdapter)
Business logic (@Application) is fully independent of infrastructure (@Adapter). No framework leaks in.
Core components depend on abstract Ports, making them easy to mock. No DB or HTTP needed in tests.
Adapters can be swapped (e.g., JPA to MongoDB, REST to gRPC) without changing the core logic.
Clear boundaries between driving and driven sides. Each adapter is isolated and replaceable.
jMolecules annotations enforce Hexagonal Architecture principles by clearly defining constraints on component interactions. By adhering to these rules: