DDD Role Hierarchy Kotlin
Erstellt am: 21. Januar 2025
Erstellt am: 21. Januar 2025
Show me how to implement a hierarchical and transitive role model in Kotlin when following DDD principles.
There are three different, but also hierarchical, entities in the management context, which can be managed by managers assigned to the one of the entities (compartment, portfolios, projects). A compartment manager can access and edit any portfolio or project within their hierarchy.
Below is a simplified example of how you might implement a hierarchical, transitive role model for compartments, portfolios, and projects in Kotlin, following Domain-Driven Design (DDD) principles. The example includes:
Of course, in a real application, you would add repositories, factories, application services, and other supporting infrastructure. This is simply a minimal, illustrative approach.
kotlinpackage com.example.domain import java.util.UUID @JvmInline value class CompartmentId(val value: UUID) @JvmInline value class PortfolioId(val value: UUID) @JvmInline value class ProjectId(val value: UUID) @JvmInline value class ManagerId(val value: UUID)
These inline classes (or “value classes” in newer Kotlin) ensure IDs are strongly typed and cannot be mixed up.
kotlinpackage com.example.domain data class Compartment( val id: CompartmentId, val name: String ) { // Additional compartment-related logic or invariants here }
kotlinpackage com.example.domain data class Portfolio( val id: PortfolioId, val name: String, val compartmentId: CompartmentId ) { // Additional portfolio-related logic or invariants here }
kotlinpackage com.example.domain data class Project( val id: ProjectId, val name: String, val portfolioId: PortfolioId ) { // Additional project-related logic or invariants here }
Notice the hierarchical relationship:
Portfolio
references the Compartment
it belongs to.Project
references the Portfolio
it belongs to (which implicitly ties it to a Compartment through that Portfolio).The core of your hierarchical role model is here. A manager role can be one of three types, each giving different permissions:
kotlinpackage com.example.domain sealed interface ManagerRole data class CompartmentManager( val compartmentId: CompartmentId ) : ManagerRole data class PortfolioManager( val portfolioId: PortfolioId ) : ManagerRole data class ProjectManager( val projectId: ProjectId ) : ManagerRole
CompartmentManager
can manage any portfolios or projects inside that compartment.PortfolioManager
can manage any projects inside that portfolio.ProjectManager
can manage only its specific project.A Manager
has an identity (ManagerId
), some descriptive info (like a name or username), and a role (the sealed interface
above).
kotlinpackage com.example.domain data class Manager( val id: ManagerId, val name: String, val role: ManagerRole ) { // Additional manager-specific behaviors or invariants here }
Your domain will need to look up portfolios and projects to navigate the hierarchy. In a typical DDD architecture, these are interfaces in the domain layer, with actual implementations in the infrastructure layer.
kotlinpackage com.example.domain interface PortfolioRepository { fun findById(id: PortfolioId): Portfolio? } interface ProjectRepository { fun findById(id: ProjectId): Project? }
(In real code, these methods might return an Either
, throw an exception, or return an optional type, depending on how you handle not-found cases.)
A domain service encapsulates the logic around “who can manage what,” especially when it spans multiple aggregates/entities. Below is a minimal example showing how you might implement hierarchical permission checks:
kotlinpackage com.example.domain class ManagementDomainService( private val portfolioRepository: PortfolioRepository, private val projectRepository: ProjectRepository ) { /** * A manager can manage a Portfolio if: * - They are the compartment manager of that portfolio's compartment * - OR they are the portfolio manager for that specific portfolio */ fun canManagePortfolio(manager: Manager, portfolio: Portfolio): Boolean { return when (val role = manager.role) { is CompartmentManager -> role.compartmentId == portfolio.compartmentId is PortfolioManager -> role.portfolioId == portfolio.id is ProjectManager -> false // project manager cannot manage entire portfolios } } /** * A manager can manage a Project if: * - They are the compartment manager of the compartment to which the project belongs * - OR they are the portfolio manager of the portfolio to which the project belongs * - OR they are the project manager of that specific project */ fun canManageProject(manager: Manager, project: Project): Boolean { return when (val role = manager.role) { is CompartmentManager -> { val portfolio = portfolioRepository.findById(project.portfolioId) ?: return false // If the project's portfolio belongs to the same compartment portfolio.compartmentId == role.compartmentId } is PortfolioManager -> { // If the project belongs to the same portfolio role.portfolioId == project.portfolioId } is ProjectManager -> { // Must match the exact project role.projectId == project.id } } } }
Below is a hypothetical snippet (e.g., in an application service) to show how it might be used:
kotlinpackage com.example.application import com.example.domain.* class ManagePortfolioService( private val managementDomainService: ManagementDomainService, private val portfolioRepository: PortfolioRepository ) { fun renamePortfolio(manager: Manager, portfolioId: PortfolioId, newName: String) { val portfolio = portfolioRepository.findById(portfolioId) ?: throw IllegalArgumentException("Portfolio not found") if (!managementDomainService.canManagePortfolio(manager, portfolio)) { throw IllegalAccessException("Manager does not have permission to manage this portfolio") } // If we get here, the manager has permission. Proceed with domain logic. val updatedPortfolio = portfolio.copy(name = newName) // Save updated portfolio... } }
In a real application, you would:
Manager
(perhaps from session/user context).Portfolio
from the repository.canManagePortfolio(...)
to ensure the manager is allowed to perform the operation.ProjectManager
can only edit certain fields).Manager
entity to hold a collection of permissible IDs instead of a single ID in its role.This approach respects core DDD principles:
CompartmentManager
, PortfolioManager
, ProjectManager
).From here, you would:
This gives you a clear, maintainable way to manage hierarchical permissions where a manager at a higher level (e.g., CompartmentManager
) automatically can manage the lower levels (portfolios, projects).
Where and how would I handle the assignment and unassignment of managers to related entity?
One entity can be managed by n managers.
Below is some guidance on where and how to handle manager assignments in a Domain-Driven Design (DDD) approach, especially if:
When you say “one entity can be managed by n managers,” that implies a many-to-many style relationship:
In DDD, there are generally two popular ways to handle this:
Each entity (e.g., Compartment
, Portfolio
, Project
) owns a collection of assigned manager references:
kotlindata class Portfolio( val id: PortfolioId, val name: String, val compartmentId: CompartmentId, val managers: MutableSet<ManagerId> = mutableSetOf() ) { fun assignManager(managerId: ManagerId) { // Domain invariants or constraints // e.g., check if managerId is already assigned if (managers.contains(managerId)) { throw IllegalStateException("Manager already assigned to portfolio") } managers.add(managerId) } fun unassignManager(managerId: ManagerId) { if (!managers.remove(managerId)) { throw IllegalStateException("Manager not assigned to this portfolio") } } }
assignManager
, unassignManager
) that enforce business invariants in one place.ManagerAssignedToPortfolio
, ManagerUnassignedFromPortfolio
).You create a dedicated domain concept for “manager-to-entity” assignments. For instance:
kotlin/** * Tracks that a specific Manager is assigned to a specific entity * (Compartment, Portfolio, or Project). */ data class ManagerAssignment( val id: ManagerAssignmentId, // value object ID val managerId: ManagerId, val entityType: EntityType, // COMPARTMENT, PORTFOLIO, PROJECT val entityId: UUID // The actual ID of the relevant entity // possibly more fields (role, constraints, date, etc.) )
You might also store the role or additional metadata here (like date assigned, who assigned them, etc.).
Then, your domain logic to create or end such an assignment is encapsulated in a domain service or in the ManagerAssignment
aggregate itself:
kotlinclass ManagerAssignmentService( private val assignmentRepository: ManagerAssignmentRepository, private val someValidationService: SomeValidationService ) { fun assignManager( managerId: ManagerId, entityType: EntityType, entityId: UUID ) { // 1. Check if it’s allowed or if a similar assignment already exists if (assignmentRepository.existsBy(managerId, entityType, entityId)) { throw IllegalStateException("This manager is already assigned.") } // 2. Possibly validate domain rules (e.g., manager’s role, entity’s constraints) someValidationService.validateAssignment(managerId, entityType, entityId) // 3. Create the new assignment val assignment = ManagerAssignment( id = ManagerAssignmentId(UUID.randomUUID()), managerId = managerId, entityType = entityType, entityId = entityId ) assignmentRepository.save(assignment) } fun unassignManager( managerId: ManagerId, entityType: EntityType, entityId: UUID ) { // 1. Load or find existing assignment val assignment = assignmentRepository.findBy(managerId, entityType, entityId) ?: throw IllegalStateException("No such assignment.") // 2. Possibly do domain checks (e.g. if manager is last manager? or is it allowed?) // ... // 3. Remove from repository assignmentRepository.delete(assignment) } }
Typically, you’d orchestrate assigning/unassigning in an Application Service (sometimes also called a “Use Case service” or “App service”). For example:
kotlinclass ManagerAssignmentAppService( private val managerAssignmentService: ManagerAssignmentService, private val compartmentRepository: CompartmentRepository, private val portfolioRepository: PortfolioRepository, private val projectRepository: ProjectRepository ) { fun assignManagerToPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { // 1. Possibly load or validate the portfolio exists val portfolio = portfolioRepository.findById(portfolioId) ?: throw IllegalArgumentException("Portfolio not found.") // 2. Delegate to the domain service (Pattern B) managerAssignmentService.assignManager( managerId = managerId, entityType = EntityType.PORTFOLIO, entityId = portfolio.id.value ) } fun unassignManagerFromPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { managerAssignmentService.unassignManager( managerId = managerId, entityType = EntityType.PORTFOLIO, entityId = portfolioId.value ) } // Similarly for compartments, projects... }
Or if you use Pattern A (directly on each entity), your app service might look like:
kotlinclass PortfolioManagementService( private val portfolioRepository: PortfolioRepository ) { fun assignManagerToPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { // 1. Load the Portfolio val portfolio = portfolioRepository.findById(portfolioId) ?: throw IllegalArgumentException("Portfolio not found.") // 2. Call the domain method portfolio.assignManager(managerId) // 3. Save the updated portfolio portfolioRepository.save(portfolio) } fun unassignManagerFromPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { val portfolio = portfolioRepository.findById(portfolioId) ?: throw IllegalArgumentException("Portfolio not found.") portfolio.unassignManager(managerId) portfolioRepository.save(portfolio) } }
You already have roles like CompartmentManager
, PortfolioManager
, ProjectManager
. If each manager’s role is more of a global statement (e.g., “I am a manager of that one compartment, so automatically all portfolios and projects inside it are in my domain”), you might not even need a separate assignment structure for each portfolio or project—they inherit it from the compartment.
However, if your domain states:
“A single entity can have multiple managers, possibly with different roles or permissions.”
…then you do need to store that somewhere. The question is whether your roles are:
If it’s “Per-Entity,” that leans toward Pattern B (separate assignment records) or a collection in each entity (Pattern A).
Use Pattern A (managers stored as part of each entity) if:
Use Pattern B (a separate ManagerAssignment
concept) if:
ManagerAssignment
aggregate, make sure the domain enforces constraints.assignManager(...)
, unassignManager(...)
on your aggregates or through a domain service (depending on your pattern).ManagerAssignedToPortfolio
) if other parts of the system need to react to these changes.By following these steps, you ensure that assignments and unassignments are first-class operations in your domain, governed by explicit invariants, and consistent with the rest of your hierarchical role model.
Can you start from scratch, with the info you have now and show me the best possible solution for this problem, when the storing of managers is also my responsibility.
Below is a from-scratch, end-to-end example of how you might design a hierarchical, transitive role model with multiple managers per entity in a Domain-Driven Design (DDD) style, using Kotlin. The guiding principles are:
ManagerAssignment
) to avoid duplicating manager collections across multiple entities, and to keep the domain logic consistent.Note: This is a reference design. In practice, you might adapt naming, add more error handling, or refine domain events, repositories, etc.
We use value classes (inline classes) to represent IDs. These ensure type-safety (e.g., CompartmentId
can’t accidentally be used where a PortfolioId
is needed).
kotlinpackage com.example.domain import java.util.UUID @JvmInline value class CompartmentId(val value: UUID) @JvmInline value class PortfolioId(val value: UUID) @JvmInline value class ProjectId(val value: UUID) @JvmInline value class ManagerId(val value: UUID) @JvmInline value class ManagerAssignmentId(val value: UUID)
For illustration, each is a simple Entity with an ID and basic fields. Notice how:
Portfolio
references which Compartment
it belongs to.Project
references which Portfolio
it belongs to.kotlinpackage com.example.domain data class Compartment( val id: CompartmentId, val name: String // Additional compartment behavior, invariants, etc. ) data class Portfolio( val id: PortfolioId, val name: String, val compartmentId: CompartmentId // Additional portfolio behavior, invariants, etc. ) data class Project( val id: ProjectId, val name: String, val portfolioId: PortfolioId // Additional project behavior, invariants, etc. )
A Manager
is itself an Entity in the domain. For example:
kotlinpackage com.example.domain data class Manager( val id: ManagerId, val name: String // Additional fields like email, status, etc. // This object does NOT hold the manager’s assignments // because we store them separately in ManagerAssignment. )
Since one entity can be managed by multiple managers — and a single manager can manage multiple entities — we handle this in a dedicated ManagerAssignment
concept (sometimes called a “mapping entity,” “join entity,” or “link” in relational terms). In DDD terms, it can be its own small Aggregate:
kotlinpackage com.example.domain /** * The type of domain object to which a manager is assigned. * One could also use a sealed class, but an enum is simpler for example. */ enum class DomainObjectType { COMPARTMENT, PORTFOLIO, PROJECT } /** * Represents an assignment of a specific Manager to a specific domain object (Compartment/Portfolio/Project). * The assigned role is implied by the "DomainObjectType": * - If type = COMPARTMENT, the manager is effectively a "Compartment Manager" * - If type = PORTFOLIO, the manager is effectively a "Portfolio Manager" * - If type = PROJECT, the manager is effectively a "Project Manager" */ data class ManagerAssignment( val id: ManagerAssignmentId, val managerId: ManagerId, val domainObjectType: DomainObjectType, val domainObjectId: UUID // Could be CompartmentId.value, PortfolioId.value, or ProjectId.value )
Note: If your domain frequently uses terms like “CompartmentManager,” “PortfolioManager,” etc., you can incorporate that directly. However, using an
enum
or sealed class forDomainObjectType
is often simpler when storing records in a database.
Why a separate aggregate?
To load and persist domain objects. In DDD, we define repository interfaces in the domain layer. Infrastructure code (e.g., JPA, SQL) implements these interfaces elsewhere.
kotlinpackage com.example.domain interface CompartmentRepository { fun findById(id: CompartmentId): Compartment? } interface PortfolioRepository { fun findById(id: PortfolioId): Portfolio? } interface ProjectRepository { fun findById(id: ProjectId): Project? } interface ManagerRepository { fun findById(id: ManagerId): Manager? } interface ManagerAssignmentRepository { fun findByManagerAndDomain( managerId: ManagerId, objectType: DomainObjectType, objectId: UUID ): ManagerAssignment? fun findAllForManager(managerId: ManagerId): List<ManagerAssignment> fun exists(managerId: ManagerId, objectType: DomainObjectType, objectId: UUID): Boolean fun save(assignment: ManagerAssignment): ManagerAssignment fun delete(assignment: ManagerAssignment) }
This domain service encapsulates the logic for assigning and unassigning managers to compartments, portfolios, or projects. It also has the potential to check domain rules before assignment/unassignment.
kotlinpackage com.example.domain import java.util.UUID class ManagerAssignmentService( private val managerAssignmentRepository: ManagerAssignmentRepository, private val compartmentRepository: CompartmentRepository, private val portfolioRepository: PortfolioRepository, private val projectRepository: ProjectRepository ) { /** * Assigns a Manager to a given domain object (Compartment, Portfolio, or Project). * - If manager is already assigned, we might throw or ignore, depending on the domain rule. * - We could also do domain validations here (e.g., does the domain object exist?) */ fun assignManager( managerId: ManagerId, domainObjectType: DomainObjectType, domainObjectId: UUID ) { // Ensure the domain object actually exists (domain rule). validateDomainObjectExists(domainObjectType, domainObjectId) if (managerAssignmentRepository.exists(managerId, domainObjectType, domainObjectId)) { // Domain rule: either ignore or throw if manager is already assigned. throw IllegalStateException("Manager is already assigned to this domain object.") } val assignment = ManagerAssignment( id = ManagerAssignmentId(UUID.randomUUID()), managerId = managerId, domainObjectType = domainObjectType, domainObjectId = domainObjectId ) managerAssignmentRepository.save(assignment) } /** * Removes a Manager’s assignment from a domain object. */ fun unassignManager( managerId: ManagerId, domainObjectType: DomainObjectType, domainObjectId: UUID ) { val existing = managerAssignmentRepository.findByManagerAndDomain(managerId, domainObjectType, domainObjectId) ?: throw IllegalStateException("No such assignment found.") managerAssignmentRepository.delete(existing) } private fun validateDomainObjectExists(domainObjectType: DomainObjectType, domainObjectId: UUID) { when (domainObjectType) { DomainObjectType.COMPARTMENT -> { val compId = CompartmentId(domainObjectId) checkNotNull(compartmentRepository.findById(compId)) { "Compartment not found." } } DomainObjectType.PORTFOLIO -> { val portId = PortfolioId(domainObjectId) checkNotNull(portfolioRepository.findById(portId)) { "Portfolio not found." } } DomainObjectType.PROJECT -> { val projId = ProjectId(domainObjectId) checkNotNull(projectRepository.findById(projId)) { "Project not found." } } } } }
We also want to check if a given Manager
can manage a particular entity. Because of hierarchical/transitive access, we must check:
Project
if they are a ProjectManager
)?Portfolio
containing this Project
, or a Compartment
containing this Portfolio
)?We can encode that logic in another domain service:
kotlinpackage com.example.domain class ManagementDomainService( private val managerAssignmentRepository: ManagerAssignmentRepository, private val portfolioRepository: PortfolioRepository, private val projectRepository: ProjectRepository ) { /** * Returns true if the manager can manage the given Portfolio. * This is true if: * 1) The manager has an assignment for the Portfolio itself (PORTFOLIO -> portfolioId) * 2) The manager has an assignment for the Compartment that owns this Portfolio */ fun canManagePortfolio(managerId: ManagerId, portfolioId: PortfolioId): Boolean { // 1. Direct assignment check val direct = managerAssignmentRepository.exists( managerId, DomainObjectType.PORTFOLIO, portfolioId.value ) if (direct) return true // 2. If manager is assigned to the compartment val portfolio = portfolioRepository.findById(portfolioId) ?: return false val compartmentAssignment = managerAssignmentRepository.exists( managerId, DomainObjectType.COMPARTMENT, portfolio.compartmentId.value ) return compartmentAssignment } /** * Returns true if the manager can manage the given Project. * This is true if: * 1) The manager has an assignment for that Project (PROJECT -> projectId) * 2) The manager has an assignment for the Portfolio that owns this Project (PORTFOLIO -> portfolioId) * 3) The manager has an assignment for the Compartment that owns that Portfolio (COMPARTMENT -> compartmentId) */ fun canManageProject(managerId: ManagerId, projectId: ProjectId): Boolean { // 1. Direct assignment check val direct = managerAssignmentRepository.exists( managerId, DomainObjectType.PROJECT, projectId.value ) if (direct) return true // 2. If manager is assigned to the portfolio val project = projectRepository.findById(projectId) ?: return false val portfolioAssignment = managerAssignmentRepository.exists( managerId, DomainObjectType.PORTFOLIO, project.portfolioId.value ) if (portfolioAssignment) return true // 3. If manager is assigned to the compartment that owns this portfolio val portfolio = portfolioRepository.findById(project.portfolioId) ?: return false val compartmentAssignment = managerAssignmentRepository.exists( managerId, DomainObjectType.COMPARTMENT, portfolio.compartmentId.value ) return compartmentAssignment } /** * Returns true if the manager can manage the given Compartment directly * (i.e., is assigned as a Compartment manager for that ID). */ fun canManageCompartment(managerId: ManagerId, compartmentId: CompartmentId): Boolean { return managerAssignmentRepository.exists( managerId, DomainObjectType.COMPARTMENT, compartmentId.value ) } }
You could generalize this further (e.g., a single method
fun canManage(managerId, domainObjectType, domainObjectId)
that recursively checks ancestor relationships), but the above is often more understandable.
Application services orchestrate domain operations. Here’s a simple example that shows how you might use the ManagementDomainService
to rename a Portfolio
if the Manager
is allowed. It also shows how to assign/unassign a manager through ManagerAssignmentService
.
kotlinpackage com.example.application import com.example.domain.* class PortfolioManagementAppService( private val portfolioRepository: PortfolioRepository, private val managementDomainService: ManagementDomainService ) { /** * Example use case: rename a portfolio (write/update). */ fun renamePortfolio(managerId: ManagerId, portfolioId: PortfolioId, newName: String) { // 1) Load portfolio val portfolio = portfolioRepository.findById(portfolioId) ?: throw IllegalArgumentException("Portfolio not found.") // 2) Check permission val canManage = managementDomainService.canManagePortfolio(managerId, portfolioId) if (!canManage) { throw IllegalAccessException("Manager $managerId is not allowed to manage Portfolio $portfolioId.") } // 3) Domain update val updated = portfolio.copy(name = newName) portfolioRepository.save(updated) // Persist changes } } class ManagerAssignmentAppService( private val assignmentService: ManagerAssignmentService ) { fun assignManagerToPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { assignmentService.assignManager(managerId, DomainObjectType.PORTFOLIO, portfolioId.value) } fun unassignManagerFromPortfolio(managerId: ManagerId, portfolioId: PortfolioId) { assignmentService.unassignManager(managerId, DomainObjectType.PORTFOLIO, portfolioId.value) } // Similar use cases for compartments or projects... }
ManagerAssignmentRepositoryImpl
(JPA/SQL, etc.) that implements ManagerAssignmentRepository
.ManagerAssignmentService
, ManagementDomainService
).canManageXxx
) in domain services, not scattered in controllers or entity code.ManagerAssignment
as a first-class concept. This handles the fact that a manager can have multiple assignments, and an entity can have multiple managers, without duplication.ManagementDomainService
encapsulates “transitive access” rules (compartment -> portfolio -> project). This logic is in one place, making it easier to change or extend later.You now have a solid foundation for a hierarchical, transitive role model with multiple manager assignments in Kotlin, fully aligned with DDD best practices.