DDD Role Hierarchy Kotlin

Erstellt am: 21. Januar 2025

Beantwortet mit O1 von Chat01

Frage

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.

Gedanke

Mapping out roles

I'm drafting a hierarchical role model in Kotlin, ensuring it aligns with domain-driven design principles. This involves defining aggregates, entities, and domain logic for managing permissions across compartments, portfolios, and projects.

Mapping authority

I'm working on defining a sealed class or abstract interface to manage roles. This setup checks authority and respects the hierarchy, ensuring managers oversee sub-entities transitive.

Structuring roles and permissions

Hmm, I’m thinking about defining data classes for entities and roles, and a domain service to manage permissions, ensuring it aligns with the hierarchical structure.

Mapping out access control

I'm detailing the "AccessControlService" to handle permissions for managers, ensuring they can manage projects based on their hierarchical roles and associated compartments or portfolios.

Crafting a code snippet

I'm preparing a code example to illustrate a domain-driven design (DDD) approach, including aggregates with distinct repositories.

Setting up domain entities

I’m constructing domain objects for compartments, portfolios, and projects. Each entity aligns with roles and permissions, ensuring a cohesive management structure.

Constructing the service

OK, let's outline an application service leveraging the domain service to manage hierarchical roles. We'll ensure clarity and include an example to illustrate the concept effectively.

Thought about hierarchical role model in Kotlin für 33 Sekunden

Antwort

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:

  • Value Objects for IDs
  • Entities for Compartment, Portfolio, and Project
  • A sealed interface for Manager Roles (CompartmentManager, PortfolioManager, ProjectManager)
  • A Manager entity that holds its role
  • A domain service to encapsulate the “who can manage what?” logic

Of course, in a real application, you would add repositories, factories, application services, and other supporting infrastructure. This is simply a minimal, illustrative approach.


1. Value Objects for IDs

kotlin
package 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.


2. Entities

Compartment

kotlin
package com.example.domain data class Compartment( val id: CompartmentId, val name: String ) { // Additional compartment-related logic or invariants here }

Portfolio

kotlin
package com.example.domain data class Portfolio( val id: PortfolioId, val name: String, val compartmentId: CompartmentId ) { // Additional portfolio-related logic or invariants here }

Project

kotlin
package 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:

  • A Portfolio references the Compartment it belongs to.
  • A Project references the Portfolio it belongs to (which implicitly ties it to a Compartment through that Portfolio).

3. Roles

The core of your hierarchical role model is here. A manager role can be one of three types, each giving different permissions:

kotlin
package 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.

4. Manager Entity

A Manager has an identity (ManagerId), some descriptive info (like a name or username), and a role (the sealed interface above).

kotlin
package com.example.domain data class Manager( val id: ManagerId, val name: String, val role: ManagerRole ) { // Additional manager-specific behaviors or invariants here }

5. Repositories (Interfaces)

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.

kotlin
package 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.)


6. Domain Service for Access Control

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:

kotlin
package 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 } } } }

Why a Domain Service?

  • The logic here spans multiple aggregates (Compartment, Portfolio, Project) and references relationships among them.
  • The actual domain rules about who can manage what are clearly represented in one place, making it easier to maintain.
  • This avoids sprinkling permission checks all over various entities.

7. Usage Example

Below is a hypothetical snippet (e.g., in an application service) to show how it might be used:

kotlin
package 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:

  1. Retrieve the Manager (perhaps from session/user context).
  2. Retrieve the Portfolio from the repository.
  3. Invoke canManagePortfolio(...) to ensure the manager is allowed to perform the operation.
  4. Perform the actual update and save.

8. Extending the Model

  • You may add more complex logic to handle partial permissions, read/write differentiation, or restrictions (e.g., a ProjectManager can only edit certain fields).
  • If managers can manage multiple compartments/portfolios/projects, you can adapt the Manager entity to hold a collection of permissible IDs instead of a single ID in its role.
  • If roles change over time, you might track role assignments as separate domain events, or store them historically.

Final Thoughts

This approach respects core DDD principles:

  1. Entities and Value Objects: We clearly differentiate them.
  2. Separation of Concerns: Hierarchical permissions reside in a domain service rather than being scattered or forced into a single entity.
  3. Ubiquitous Language: We use clear domain terms (CompartmentManager, PortfolioManager, ProjectManager).

From here, you would:

  • Implement the infrastructure side of repositories (e.g., using JPA, JDBC, etc.).
  • Build application services (use cases) around these domain rules, orchestrating the domain logic and calling out to repositories.
  • Potentially add domain events to handle side effects of updates in compartments, portfolios, or projects.

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).

Frage

Where and how would I handle the assignment and unassignment of managers to related entity?
One entity can be managed by n managers.

Gedanke

Mapping out domain logic

I’m charting manager assignments for entities using DDD. Considering an Aggregate or an assignment object to effectively manage roles.

Tracking manager assignments

OK, let me see. I'm focusing on managing manager assignments by storing their references within domain objects, each keeping a Set of manager IDs, ensuring domain logic remains intact.

Centralizing assignments

I’m thinking about a "ManagerAssignment" aggregate to gather manager assignments, their roles, and entity details. This approach simplifies domain logic and reduces duplication.

Mapping request handling

Assigning managers involves an application service, a domain service, and domain logic. Updates are stored in repositories. Progressing through each step ensures smooth implementation.

Crafting manager assignment

I'm thinking through using a domain service or entity method for manager assignment, incorporating event handling, and maintaining a manager collection within the entity to ensure domain logic coherence.

Thought about manager assignment and unassignment für 43 Sekunden

Antwort

Below is some guidance on where and how to handle manager assignments in a Domain-Driven Design (DDD) approach, especially if:

  1. An entity (Compartment, Portfolio, Project) can be managed by multiple managers.
  2. You want clear, explicit domain logic for assigning and unassigning managers.

1. Common Patterns for Handling Assignments

When you say “one entity can be managed by n managers,” that implies a many-to-many style relationship:

  • One manager might manage multiple entities (or one).
  • One entity might be managed by multiple managers.

In DDD, there are generally two popular ways to handle this:

Pattern A: Manage Assignments Directly on the Entity

Each entity (e.g., Compartment, Portfolio, Project) owns a collection of assigned manager references:

kotlin
data 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") } } }

Advantages

  • Keeps all management-related state in the aggregate that is being managed.
  • Clear domain methods (assignManager, unassignManager) that enforce business invariants in one place.
  • Easy to publish domain events from within the aggregate (e.g., ManagerAssignedToPortfolio, ManagerUnassignedFromPortfolio).

Potential Drawbacks

  • If you have a very large, complex system, each entity’s collection of managers could grow large. You’ll need to ensure you handle it gracefully in persistence (e.g., bridging tables in relational DB).
  • In some domains, you might also need a shared manager assignment concept across multiple entities. Storing managers on each entity can lead to duplication or more complicated logic if you have many entity types.

Pattern B: A Separate “Assignment” Aggregate (or Domain Concept)

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:

kotlin
class 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) } }

Advantages

  • Central place to manage “who manages what” across many entity types.
  • Prevents duplication of manager collections in each entity.
  • Allows for more complex assignment logic (e.g., time-based assignments, multiple roles, historical records).

Potential Drawbacks

  • Requires a second look-up each time you want to know “who manages this entity?” or “which entities does this manager manage?”. You’ll typically handle that in queries or via a domain service.
  • Might feel more infrastructure-y if your domain’s Ubiquitous Language really does revolve around “Portfolios have managers.” Storing them inside the Portfolio entity can be more natural if that’s how your domain experts describe it.

2. Where to Invoke These Methods?

Typically, you’d orchestrate assigning/unassigning in an Application Service (sometimes also called a “Use Case service” or “App service”). For example:

kotlin
class 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:

kotlin
class 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) } }

3. Synchronizing with Your Role Model

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:

  1. Global to the manager (i.e., “Jane is a CompartmentManager for Compartment#1,” and that automatically implies all sub-entities are managed by her).
  2. Per-Entity (i.e., “Jane is a Manager for Portfolio#X, Bob is also a Manager for Portfolio#X,” etc.).

If it’s “Per-Entity,” that leans toward Pattern B (separate assignment records) or a collection in each entity (Pattern A).


4. Decision Criteria

Use Pattern A (managers stored as part of each entity) if:

  • Your domain experts say “A Portfolio has managers,” and they want to see that relationship in one place.
  • The assignment logic and validation revolve primarily around the Portfolio (or Project, etc.) invariants.
  • You do not have a huge performance concern about large sets of managers per entity.

Use Pattern B (a separate ManagerAssignment concept) if:

  • You have more complex assignment logic (e.g., time-based, multiple roles, you need historical assignment records, etc.).
  • You want to manage cross-entity assignments in a single, consistent way.
  • The domain explicitly talks about “assignments” as a concept in its own right.
  • You need the same logic for Compartment, Portfolio, Project, or other new entity types that might be added over time.

5. Final Thoughts

  1. Keep the assignment logic in the Domain. Whether you store manager references inside each entity or in a dedicated ManagerAssignment aggregate, make sure the domain enforces constraints.
  2. Expose clear domain methods like assignManager(...), unassignManager(...) on your aggregates or through a domain service (depending on your pattern).
  3. Publish domain events (e.g., ManagerAssignedToPortfolio) if other parts of the system need to react to these changes.
  4. Use an Application Service to handle the orchestration: load the entity, call the domain logic, save changes.
  5. Synchronize with your existing role model (CompartmentManager, PortfolioManager, etc.) so that assignment in code accurately reflects the user’s “hierarchical vs. specific” management rights.

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.

Frage

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.

Gedanke

Building a solution

I'm mapping out a Domain-Driven Design (DDD) approach, thinking compartments, portfolios, projects, and hierarchical roles. Each level manages its entities, with managers storing their assignments.

Mapping out manager roles

I'm exploring different designs: including value objects, integrating manager roles, and managing multiple manager assignments per entity. Separate assignments for clarity seem promising.

Balancing storage needs

I'm working through managing manager assignments efficiently: avoiding redundancy, enabling cross-entity management, and aligning with hierarchical logic for determining management roles. Progress is steady as I outline these principles.

Crafting domain models

I’m developing a domain model with manager entities and assignments for compartments, portfolios, and projects. This approach ensures clear and efficient management hierarchies.

Formulating the domain model

I'm classifying value classes and entities, establishing manager assignments, and crafting a domain service to verify management rights, leveraging hierarchical logic for efficient access control.

Mapping out domain services

I’m linking manager assignment functionalities and repositories, exploring application services for external use, and considering implementing a sample code snippet in Kotlin to illustrate the solution.

Thought about manager assignments in DDD für 33 Sekunden

Antwort

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:

  1. Clean Separation of Concerns: Keep the domain model expressive and consistent with the Ubiquitous Language (Compartment, Portfolio, Project, Manager, etc.).
  2. Hierarchical, Transitive Access: A manager of a Compartment can manage any Portfolio or Project within that Compartment; a Portfolio manager can manage Projects within that Portfolio; and a Project manager can manage only that Project.
  3. Support Multiple Managers per Entity: One entity may have multiple managers assigned.
  4. Explicit Assignment Model: We’ll store manager-to-entity assignments in a separate concept (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.


1. Value Objects for IDs

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).

kotlin
package 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)

2. Aggregates: Compartment, Portfolio, Project

For illustration, each is a simple Entity with an ID and basic fields. Notice how:

  • A Portfolio references which Compartment it belongs to.
  • A Project references which Portfolio it belongs to.
kotlin
package 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. )

3. Manager Entity

A Manager is itself an Entity in the domain. For example:

kotlin
package 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. )

4. ManagerAssignment Aggregate

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:

kotlin
package 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 for DomainObjectType is often simpler when storing records in a database.

Why a separate aggregate?

  • Avoids sprinkling sets of managers inside every entity.
  • Makes it easier to manage or query “Which manager is assigned to which entity?”
  • If your domain logic includes additional info (e.g., date assigned, who assigned, role constraints), you can store it here.

5. Repositories

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.

kotlin
package 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) }

6. Domain Service: ManagerAssignmentService

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.

kotlin
package 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." } } } } }

7. Domain Service: ManagementDomainService (Access Control)

We also want to check if a given Manager can manage a particular entity. Because of hierarchical/transitive access, we must check:

  1. Does the manager have a direct assignment for this entity (e.g., a Project if they are a ProjectManager)?
  2. Or does the manager have an assignment for an ancestor entity (e.g., a Portfolio containing this Project, or a Compartment containing this Portfolio)?

We can encode that logic in another domain service:

kotlin
package 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.


8. Application Services (Use Cases)

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.

kotlin
package 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... }

9. Putting It All Together

  1. Create your Repositories (infrastructure/persistence layer). You might have something like ManagerAssignmentRepositoryImpl (JPA/SQL, etc.) that implements ManagerAssignmentRepository.
  2. Instantiate Domain Services by passing the repositories into their constructors (ManagerAssignmentService, ManagementDomainService).
  3. Build Application Services that orchestrate the domain logic. For example, when the user triggers “Rename Portfolio” or “Assign Manager,” your app code calls these application services, which in turn call the domain services.
  4. Check Access wherever you do domain updates.
  5. Publish Domain Events if other parts of the system need to react to “manager assigned” or “portfolio updated,” etc.

10. Why This is a “Best Possible Solution” (Guiding Rationale)

  1. DDD-Style Separation: We keep the domain logic (canManageXxx) in domain services, not scattered in controllers or entity code.
  2. Explicit Assignments: Rather than storing sets of managers on each aggregate, we use 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.
  3. Hierarchical Logic: The ManagementDomainService encapsulates “transitive access” rules (compartment -> portfolio -> project). This logic is in one place, making it easier to change or extend later.
  4. Application Services: We have small, clean application services that orchestrate domain operations without containing business logic.
  5. Extendable: Adding new roles or new entity types is straightforward. If you add, say, “Department,” you only need to update the enumerations, repositories, and domain service checks.

You now have a solid foundation for a hierarchical, transitive role model with multiple manager assignments in Kotlin, fully aligned with DDD best practices.

Diesen Q&A teilen