0

I'm wondering how i can implement what Im calling "business policies" in my code? For discussion purposes, let's say I have two separate contexts - synchronizeUsers and Licensing.

The synchronize users bounded context is a downstream component that connects to a Open Host Service, gets a list of changes to users, persists those changes to my local database.

if the sync process finds a deleted user, we mark it as deleted in our db and technically this process is complete. However, there are other components in the system that need to know about this deleted user and take action, depending on the business rules. For example, let's say that after a user is deleted, I need to revoke some licenses. This will be handled by the Licensing Context. The licensing context shares the user entity that the synchronize user context creates.

In order to make the code more readable and organized, I'm trying to avoid concepts like "license managers" and I'm also trying to avoid calling licensing logic directly from the user sync logic.

Instead, I'm currently looking at how to implement a mediator pattern so things are more loosely coupled.

But the part where I'm stuck is deciding what specifically the mediator should do. I'll try to demonstrate the problem by describing specific use cases.

Use Case A

I will use a mediator that just passes messages along to registered / interested parties. So something like this:

namespace Widgets.Core.Application.Communication
{
    public class MessageBroker: Mediator
    {
   
        private List<WidgetBoundedContexts> contexts = new();
        
        //register a single context / business component.
        public override void Register(WidgetBoundedContexts context)
        {
            context.SetMessageBroker(this);
            this.contexts.Add(context);
        }

        //register multiple contexts at the same time.
        public void RegisterContexts(params WidgetBoundedContexts[] contexts)
        {
            foreach(var context in contexts)
            {
                this.Register(context);
            }
        }
        public override void Send(string from, string message)
        {
            this.contexts.ForEach(m => m.Receive(from, message));
        }
        public override void SendTo<T>(string from, string message)
        {
            this.contexts.OfType<T>().ToList().ForEach(m => m.Receive(from, message));
        }
    }

    }
}

The user sync will sends a message to its mediator to say "user has been deleted", and then anyone that has registered with the mediator as a "UserDeletionPolicyListener" (type ) will be notified.

The requirement here is that these listeners are already running / instantiated. Also, each listener will have their own list of to-dos associated with a deleted user. For example, if we have another context that should auto-unsubscribe them to a bunch of mailing lists, let's say.

Two problems here: a) the objects that need to do something may or may not already be instantiated. b) I want to provide a holistic overview of what our corporate "delete user" policy is. This doesn't provide that for us.

Use Case B

The mediator does more than just accept pass messages along to listeners. It will track policies.

So here's where I'll try to add some pseudo code to give you a clearer picture of what I'm trying to accomplish. Here's what I think a policy class might look like:

namespace Widget.Core.Application.Policies
{
    public abstract class WidgetPolicy
    {
        public string PolicyName { get; set; }

        public WidgetPolicy(string name)
        {
            this.PolicyName = name;
        }
        public abstract bool Policy(params object[] policyData);

    }
    public class UserPermanentlyDeletedPolicy:  WidgetPolicy
    {
        public string PolicyName { get; }

        public UserPermanentlyDeletedPolicy(string policyName) : base(policyName)
        {
            this.PolicyName = policyName;
        }
        public override bool Policy(params object[] policyData)
        {
            /*
                State:  User sync has already removed the user from Active User table.
                It should have also added a permanent deleltion date in the Deleted Table for said user.
            */
            
            try 
            {
               //call the Licensing logic here. 
               LicenseManager lm = new LicenseManager(); 
               lm.RemoveLicenses(policyData)

               //unsubscribe from all mailinglists
               MailingListManager mlist = new MailingListManager(); 
               mlist.UnsubscribeAll(policyData)

               return true;
            }
            catch (exception)
            { 
              return false;
            }
        }

    }
}

Questions about use case B

  1. Is the mediator pattern the right pattern to use? If i just need something to trigger a generic policy from user sync then maybe i don't need it. I guess then any context that needs to trigger a series of unrelated events will all be dependant on this new "policy" component.

  2. If mediator is still useful / applicable, I'm not quite sure how to adjust the message broker / mediator for Use case B.

Any suggestions / comments would be appreciated.

Edit 1

After reading the answers below, I would like to just clarify one thing to be sure I'm on the same page.

If I'm understanding correctly, the reason why Mediator is NOT the right pattern here is specifically because:

  • Multiple clients/contexts can trigger the same policy via the mediator, but not all of them would be interested in the results from the other callers.
  • mediator should only have ONE handler for ONE event.

Secondly,in @Ewans answer, the material point is that mediator doesn't free me from maintaining relationship between user sync and deletion policy? I don't mind maintaining some loose relationship, as long as its controllable. I thought that was the point of the mediator. Don't put the logic to call B inside of A. Put it inside of mediator. This way you don't end up with a tangled mess of dependencies but more of a spoke/hub design. Sorry if I'm missing something basic. I just want to make sure I really understand your point of view.

With the database approach, I guess I could:

  • have the user sync write to a table that indicates "start the deletion policy"
  • create a background job that's scheduled to run every X number of minutes. This job would check the table and if it finds a record then it starts the process / policy logic. I don't like this because it's very chatty. And can potentially lead to race conditions. I would like the user sync to trigger the policy if and only if needed.

Last comment, in case it helps, with regards to using a message queue or service bus. I was considering that but I started to check out the mediator pattern based on my question posted here:

Is a message queue the right pattern to use for intra-module communication in a .net core application

2 Answers 2

2

I'm also trying to avoid calling licensing logic directly from the user sync logic.

The mediator pattern doesn't really help you avoid this, you are still calling the function, you have just exposed the hook to objects that don't have a direct reference to the calling code.

For your use case I would use a message queue or even just a database of "jobs to do"

When the sync job deletes a user, it can put a "PerformUserDeletionPolicyTasks" message on the queue or in the database and carries on.

A separate worker process picks up these jobs and does whatever you want, marking them complete, recording errors, rerunning failed tasks etc.

Now you can completely describe the "User Deletion policy" in the isolated code of that worker process, the sync job is unaffected by any errors or delays, and its completely auditable.

3
  • good to see you again. lol. Please see Edit 1. I just have one or two questions about how to avoid the background job from being too chatty.
    – dot
    Jun 15, 2023 at 13:52
  • ahh ok linked questions!! yeah i think the mediator pattern is a mistake for your setup. either go full MQ and have the jobs running in separate processes, or just have the one process and make the function calls normally. The mediator pattern is only useful where you have multiple components which need to communicate but cant reference each other. like in a declarative front end or similar
    – Ewan
    Jun 15, 2023 at 18:13
  • I disagree with @flater (as usual :) ) you can have the mediator with multiple listeners just like you can have multiple event handlers. But that doesn't really help one way or another, you main thread still crashes if the mediator event handlers crash, unless you are in a constrained framework you might as well just pass in a reference and make the call directly
    – Ewan
    Jun 15, 2023 at 18:19
1

A mediator is not an event bus or message queue. It's a very common misconception, e.g. there are countless requests on the Mediatr support forum to allow multiple handlers to act on a request, but it's just not what it is built to do.

A mediator connects you to one handler. While it's fairly easy to then subsequently go "why not more than one?", there is a lot of complexity hidden in that additional behavior:

  • When handlers return values, your mediator would need to return a collection of return values to the caller. Since the caller cannot pre-emptively know how many handlers are registered, therefore every call to a mediator should expect to receive a collection?
  • What happens when one handler does not do its job?
    • Should all handlers be rolled back?
    • Should the result collection contain an intentionally empty value to indicate that these was a handler that did not complete?
      • What if there's no value being returned, how would you know?

You could answer all of my questions in a way that you need it to work, sure, and then you would have a working solution for your problem; but those answers are not universally applicable. This opens up a whole new can of worms and that's just not what the mediator pattern is about.

There's nothing wrong with what you're intending to do but the mediator pattern just isn't the right fit for the job. Look into message queues, service buses and event hubs; they're closer aligned to what you need.

1
  • thanks for the comments / answer! Can you please check out Edit 1 in my post incase it helps / changes your comments? In particular I'm referring to the link I included to my first question on this site.
    – dot
    Jun 15, 2023 at 13:53

Your Answer

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

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