Overview

The jMolecules annotations help enforce Hexagonal Architecture principles, ensuring modularity, separation of concerns, and technology independence. This document explains the constraints (must depend on and must not depend on), provides examples, and outlines how these concepts are applied in a real-world project.


Dependency Rules: Must Depend On and Must Not Depend On

In Hexagonal Architecture, dependency rules ensure a clear boundary between your application’s core logic and its infrastructure or technology-specific details.

1. Must Depend On

  • Definition: Specifies which components or interfaces a given component is allowed to rely on or interact with.
  • Purpose: Ensures proper communication between components and avoids direct access to unrelated parts of the system.
  • Example: A PrimaryAdapter must depend on a PrimaryPort to ensure it communicates with the core application only through defined interfaces.

2. Must Not Depend On

  • Definition: Specifies which components a given component is not allowed to rely on or interact with.
  • Purpose: Prevents tight coupling and enforces architectural boundaries, keeping the system modular and technology-independent.
  • Example: The application core (@Application) must not depend on @Adapter to ensure the core logic remains decoupled from technology.

jMolecules Annotations and Their Constraints

Here’s a table summarizing the allowed and restricted dependencies for each jMolecules annotation:

AnnotationMust Depend OnMust Not Depend OnRepresents
@Adapter@Port@ApplicationTechnology-specific implementation driving or implementing a Port.
@Application@Port@AdapterCore business logic, independent of technology or framework.
@PortNoneTechnology-specific codeA contract between the application and the outside world.
@PrimaryAdapter@PrimaryPort@SecondaryPort, @AdapterTechnology-specific implementation driving the application via Port.
@PrimaryPortNone@Adapter, @SecondaryPortCore interface for external systems to interact with the application.
@SecondaryAdapter@SecondaryPort@PrimaryPort, @AdapterTechnology-specific implementation for external systems driven by Port.
@SecondaryPortNoneTechnology-specific codeCore interface for application-driven communication with external systems.

Example: User Registration System

Let’s build a user registration system to see these annotations and rules in action.

Core Interfaces and Adapters

  1. PrimaryPort: UserService

    • Exposes methods like registerUser(user: User) for external systems to drive the application.
  2. PrimaryAdapter: HttpController

    • Implements a REST API, delegating user registration tasks to UserService.
  3. SecondaryPort: UserRepository

    • Abstracts persistence operations, such as saving users to a database.
  4. SecondaryAdapter: JpaUserRepository

    • Implements UserRepository using JPA to interact with the database.

Code Example

PrimaryPort (UserService)

@PrimaryPort
interface UserService {
    fun registerUser(user: User): String
}

PrimaryAdapter (HttpController)

@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)
    }
}

SecondaryPort (UserRepository)

@SecondaryPort
interface UserRepository {
    fun save(user: User): String
    fun findById(userId: String): User?
}

SecondaryAdapter (JpaUserRepository)

@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)
    }
}

Application Core

@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)
    }
}

Dependency Rules in Action

  1. HttpController (PrimaryAdapter):

    • Must Depend On: UserService (PrimaryPort).
    • Must Not Depend On: UserRepository (SecondaryPort) or JpaUserRepository (SecondaryAdapter).
  2. UserServiceImpl (Application):

    • Must Depend On: UserRepository (SecondaryPort).
    • Must Not Depend On: JpaUserRepository (SecondaryAdapter).
  3. JpaUserRepository (SecondaryAdapter):

    • Must Depend On: UserRepository (SecondaryPort).
    • Must Not Depend On: UserService (PrimaryPort) or HttpController (PrimaryAdapter).

Why These Rules Matter

  1. Encapsulation:
    • Business logic (@Application) is independent of infrastructure (@Adapter).
  2. Testability:
    • Core components depend on abstract Ports, making them easy to mock.
  3. Technology Independence:
    • Adapters can be swapped (e.g., JPA to MongoDB) without changing the core logic.

Conclusion

jMolecules annotations enforce Hexagonal Architecture principles by clearly defining constraints on component interactions. By adhering to these rules:

  • Your system becomes more modular, testable, and maintainable.
  • The business logic remains independent of technology-specific details.