0

In my example app there are admins and modules. Each admin may have some permissions to access a module. Also please note that I am assuming "simplified" DDD/CQRS where read models are allowed to read BC entities (entities from which aggregates in BC are "made" of). Also I am not considering event sourcing here. I only consider domain events which may "cause" some integration events (which are guaranteed to be delivered to interested parties).

I defined two BCs:

  • admin BC
    • admin_aggregate (root aggregate)
    • admin_module_permissions (entity)
  • module BC
    • module_aggregate (root aggregate)

Admin aggregate looks something like this (simplified):

enum ModulePermissions {
  read,
  write
}

class AdminModulePermissions {
  Guid module_guid;
  ModulePermissions permissions;
}

class Admin {
  void grantAccessToModule({
    Guid module_guid,
    ModulePermissions permissions,
  }) {
    // ...
    // AdminModulePermissionsGrantedDomainEvent(admin, module, permissions)
    // or
    // AdminModulePermissionsChangedDomainEvent(admin, module, permissions, oldPermissions)
  }

  void revokeAccessToModule({
    Guid module_guid,
  }) {
    // ...
    // AdminModulePermissionsRevokedDomainEvent(admin, module, oldPermissions)
  }
}

Now I need module read model that lists all modules to which admin has access to. Ideally would be to "get" list of modules admin has access to on higher level (identity) and pass it as a param to module read model like listModulesByGuids(List<Guid> moduleGuids). In such case module read model would not have to know anything about any module permissions. However as there might be thousands of modules that single admin has access to, it would be very inefficient to pass thousands of module guids/permissions to module read model... I have some ideas how to handle it but I am not sure which (if any) will not "break" DDD principles:

  1. Idea #1:

Leave admin BC and permission grant/revoke use cases as is, and fire integration events AdminModulePermissions(Granted | Changed | Revoked)IntegrationEvent. Then make module read model to listen for those events and keep admin module permissions in read model entity. This way module permissions will be "duplicated" in admin BC aggregate and module read model. Module read model could then provide listModulesForAdmin(Guid admin_guid) method which would list modules (along with permissions) for particular admin.

  1. Idea #2:

Keep module permissions entirely in module BC instead of Admin BC. This way there is no problem with listing modules for admin. However permissions are stored in different place that admin BC which can be as good as it can be wrong :D - not sure here. It's nice to have all permissions centralised in one place (admin BC), but on the other hand if there were more parts of the system (in addition to modules) that required other permissions then it could become complicated to manage all those in Admin BC. Again on the other hand if permissions to different parts were dependant somehow then it would be smarter to keep them in one place (admin bc). Please note that integrations would be used only by module read model and not module BC. So authorization before module BC use cases would still involve admin BC (it's read model actually, to check if particular admin can write/modify particullar module.

  1. Idea #3

This is something that could keep permissions entirely in Admin BC and on the other hand allow filtering in module read model. So, if there were thousands of modules then it would not make sense to return them all at once from module read model - they have to be paginated instead. My idea is that pagination could be done in admin read model and then just small set of module guids would be passed to module read model listModulesByGuids(justTenGuidsOrSomething). However pagination (reading admin read model) would have to be done in higher level like in controller or api gw or something like this. It's doable but it feels like unnecessary complexity. But maybe it's the way to go? I think it's called "view composition" (combining separate read models on higher level (api gw?).

So I am leaning towards #1 and #2 ideas because they allow to keep read models self sufficient but on the other hand this requires to keeps thing either duplicated or design "not-ideal" BCs. The question is if it's better to keep admin permissions centralized in admin BC and just spread them to other parts of the system (to read models not BCs!) by using integration events (with eventual consistency as permissions won't be available in other parts immediately). Or to keep permissions in particular part of the system. Or maybe #3 is the way to go?


Edit: Just realised that #3 won't work if it's required to order modules by any module fields (in this case it's not possible to paginate modules that admin has access to in admin read model).

1 Answer 1

2

Small intuition: It's hard to conclusively prove or to point out the cause of it, but I get the feeling that you're coupling your concerns much too tightly, and it's having an overbearing effect on every consideration you have to make. But since I can't conclusively prove that, I need you to re-evaluate if your design is a given (due to requirements you haven't communicated here), or whether you can relax your architecture by separating your concerns more.


Contradiction

(quote 1) I need module read model that lists all modules to which admin has access to.

(quote 2) However as there might be thousands of modules that single admin has access to, it would be very inefficient to pass thousands of module guids/permissions to module read model.

If the problem you raise here (quote 2) is a genuine concern, then the business requirement (quote 1) is invalid. Otherwise, if the business requirement is strictly correct, then your concern (quote 2) is inherently a non-issue.

This isn't an inherent conflict. It's a conflict of two opinions you hold on what is necessary and what is overkill. The solution isn't found in a technical implementation, it's found by first figuring out what it is you want, and then the technical implementation will follow.

I'm not going to play 20 questions with you to find what it is you want (StackExchange is not the right format for this), but based on the above quotes you are either going to have to let go of your concern, or significantly alter your business requirement.

The core issue

Your question essentially boils down to where you store the permissions: aggregate them, or keep them in their respective modules?

This very much depends on context. I've never come across a DDD codebase with literally thousands of bounded contexts. I suspect that your "modules" are dynamic data, i.e. constructed during runtime by an end user, not an architectural part of your domain. But that is just inference, I can't be sure.

If you literally have thousands of bounded contexts in your domain (i.e. they are known at compile time), then I think your overall architecture is disproportionate to the problem domain you find yourself in. The sheer amount of logic required for each bounded context to exist on its own, multiplied by the thousands of bounded contexts you're talking about, puts your minimum codebase size well beyond what a "simplified DDD" (by your own words) can reasonably manage.

You can try and make it work as much as you can, but it'd be better in the long run if you move to an architecture that is more suited to such a massive codebase. I'd start considering microservices here.

If your modules are indeed dynamic data (i.e. only known at runtime) and not individually part of your domain concepts, then there is only one solution here: reusable permission logic that applies to any dynamically created module. But your question seems to assume that all your modules are individually known by name (since you're storing permissions in them), which in turn suggests that your modules are known at compile time and therefore are not dynamic data.

Like with other things in this question, you find yourself at a crossroads of conflicting ideas, and no disambiguation as to which is correct. The solution is found by actually explaining what is required, and what is a concrete problem. Not what you like most, not what you think might possibly be a future problem. Stick to the facts.

Idea #1

Duplication of data leads to the possibility of these duplicate sources contradicting one another. Especially in cases of authorization, that's an allround bad idea.

Idea #2

None of this is a concrete explanation. It's a rant about two conflicting ideas you like. If you need help with this, you need to do the legwork and talk about the facts rather than what you like.

Idea #3

Pagination is essentially that change to your business requirement (quote 1) that I was talking about. It relaxes the constraint that all permitted modules must be returned, into saying some must be returned. That is (pretty much by definition) a solution to your issue with having to return all modules in a single go (quote 2).

But then you do the same thing as you did before, and you contradict your own solution:

(quote 3) It's doable but it feels like unnecessary complexity.

This is the same conflicting logic again. If it actually is unnecessary (quote 3), then the issue you pointed out before (quote 2) is a non-issue (since you just called it unnecessary).

Otherwise, if quote 2 is indeed an actual issue, then quote 3 in inherently invalid since this pagination logic can therefore not be unnecessary then.

Conclusion

You have two ideas, and you are letting each idea defeat the other one. This is not a meaningfully answerable question, other than just telling you to pick one solution and run with it.

You're failing to start because you're trying to make sure everything is perfect the first time round. And while it's very understandable, it's something you're going to have to get over.

The way to allay that fear isn't found by holding off on starting development or pingponging between two opposite ideas; but rather by starting with one idea and sticking to clean coding practices, because it will minimize the cost of having to make changes in the future if it turns out that your first attempt wasn't perfect.

Spoiler alert; the first implementation is almost never the right one. You're always going to have to make changes, whether it's because you made a mistake, the requirements were miscommunicated, or the client has simply changed the requirements. You can't control or predict any of this, so all you can do is ensure that your codebase is as change-friendly as it can be, so that you can easily respond to a situation that requires you to change part of your approach.

1
  • Thanks for long answer and sorry that I was not clear enough. I dived too deep in design language while I should clearly describe requirements. Modules are dynamic (one module is actually one db entity and its known only at runtime). You can think of modules like they are blog posts to which admins have permissions. Also by meaning "list all modules for admin" I assumed that they will be paginated.
    – user606521
    Feb 12, 2021 at 14:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.