Published on

Functions, objects, actors: deliverying results

Authors
Kudos! 🙏🏻

This tale wouldn't be here if I didn't have conversations with Grzegorz Galezowski.

Thanks Grzegorz Galezowski for your patience, your knowledge and sharing your experience with me!

This blog post is full of reflections after reading the discussion between Grzegorz and Mark Seeman

The goal

The intent of this blog post is to show that there are multiple ways of yielding results in our systems.

Grzegorz presented very interesting approach on using Command design pattern - I totally recommend exploring it!

An assignment

Imagine that we were challenged with implementing a simple use case in our system - an intent to assign a worker to a specific assignment.

Initially, we know that this use case is going to be realized using HTTP API, which means that our ASP.NET Core Minimal API handler or Controller's action method will be the entrypoint to our application.

The knowledge we are going to apply is the following:

  1. A consumer requests assigning someone to an assignment with given identifier.
  2. The system is going to fetch the first available worker.
  3. A specific assignment is loaded.
  4. The worker is assigned.
  5. The assignment result is returned to the consumer.

Straightforward, right?

(you might be wondering what do I meanby "the knowledge we are going to apply" - you might be interested in The ambiguity of application)

We defined inputs (the assignment identifier), the processing steps (2-4) and the output (the assignment result).

Let's try to implement it using one of the most common approaches - command handler returning a result.

Command handler returning a result

Let's start from the outside-in perspective - so the entrypoint to our system - an HTTP Minimal API handler - anonymous lambda.

app.MapPost("/assignments/{assignmentId:guid}/assign", async (Guid assignmentId, IDispatchCommands dispatcher) =>
{
    var result = await dispatcher.Dispatch<AssignWorkerCommand, AssignmentResult>(new AssignWorkerCommand(assignmentId));

    return result.Match() // ❌ bzzz, compile-time error - what should we supposed to match?
});

In our initial behavior specification (specification? you might be interested in The ambiguity of "TDD") we covered the happy path. We have not describe what results do we expect when things go wrong.

Let's say our process experts told us that we have three failure scenarios:

  • there is no workers available
  • assignment has already assigned worker
  • a worker is not qualified to do a given assignment

So we can represent this knowledge in the code:

app.MapPost("/assignments/{assignmentId:guid}/assign", async (Guid assignmentId, IDispatchCommands dispatcher) =>
{
    var result = await dispatcher.Dispatch<AssignWorkerCommand, AssignmentResult>(new AssignWorkerCommand(assignmentId));

    return result.Match( // ❌ bzzz, compile-time error - don't we need to implement it?
        successfullyAssigned: assignmentResult => Results.Ok(new { assignedWorkerId = assignmentResult.WorkerId }),
        noWorkersAvailable: () => Results.UnprocessableEntity(new { message = "No workers available" }),
        alreadyAssigned: assignmentResult => Results.Conflict(new { message = "Assignment already has assigned worker", error = "assignments:assigning:no-workers-available", alreadyWorkerId = assignmentResult.WorkerId }),
        notQualified: assignmentResult => Results.UnprocessableEntity(new { message = "Worker is not qualified to do this assignment", error = "assignments:assigning:worker-not-qualified", unqualifiedWorkerId = assignmentResult.WorkerId }),
        otherError: message => Results.UnprocessableEntity(new { message = message; error = "assignments:assigning:other-error"})
    );
});

We are engineers, so we know there the default set of errors to handle - "other errors" - ones we don't know how to deal with.

Let's put a error bag for these.

Our HTTP interaction handler uses the returned result from processing.

Of course, nothing yet was implemented, so let's move on!

Let's return a processing result!

Let's define inputs and outputs:

// the input - the command as data
public record AssignWorkerCommand(Guid AssignmentId) : ICommand;

// the output - the value object as the result of processing
public abstract record AssignmentResult
{
    public abstract TResult Match<TReturn>(
        Func<AssignmentResult, TReturn> successfullyAssigned,
        Func<TReturn> noWorkersAvailable,
        Func<AssignmentResult, TReturn> alreadyAssigned,
        Func<AssignmentResult, TReturn> notQualified,
        Func<string, TReturn> otherError    
    );

    public sealed record SuccessfullyAssigned(Guid WorkerId) : AssignmentResult
    {
        public override TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError
        )
        {
            return successfullyAssigned(this);
        }
    }

    public sealed record NoWorkersAvailable() : AssignmentResult
    {
        public override TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError    
        )
        {
            return noWorkersAvailable();
        }
    }

    public sealed record AlreadyAssigned(Guid WorkerId) : AssignmentResult
    {
        public override TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError
        )
        {
            return alreadyAssigned(this);
        }
    }
    
    public sealed record WorkerNotQualified(Guid WorkerId) : AssignmentResult
    {
        public override TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError
        )
        {
            return notQualified(this);
        }
    }

    public sealed record OtherError(string errorMessage) : AssignmentResult
    {
        public override TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError
        )
        {
            return otherError(errorMessage);
        }
    }
}

Ok, it got a bit verbose, but we have a clear structure of our processing result.

We expressed a common, abstract concept for "assignment result" and we have a concrete implementations of it, covering variability of cases.

I hear you dear Reader: "omg, so much boilerplate" and it's true, not denying it.

We could of course use OneOf nuget package for discriminated unions, but let's imagine we decided to go with dependency-free approach that is built in the language we use.

Processing knowledge application

Now it's time to build command handler - an application of the process steps we got familiar with.

One of them was: "2. The system is going to fetch the first available worker."

We don't care how this capability is going to be delivered, we focus on what instead - to do so we defined the responsibility: (responsibility? you might be interested in The ambiguity of interfaces)

public interface IProvideWorker
{
    public Task<Maybe<Worker>> ProvideWorker();
}

Our command processor requires capabilities for loading and saving assignments, so let's express them bound to a specific role:

public interface IAssignmentRepository
{
    public Task<Maybe<Assignment>> Load(Guid assignmentId);
    public Task Save(Assignment assignment);
}

Equipped with those capabilities, we are ready to represent our understanding using code:

public record AssignWorkerCommandHandler(IProvideWorker workerProvider, IAssignmentRepository assignmentRepository)
    : ICommandHandler<AssignWorkerCommand, AssignmentResult>
{
    public async Task<AssignmentResult> Handle(AssignWorkerCommand command)
    {
        try
        {
            var worker = await workerProvider.ProvideWorker();
            if(!worker.HasValue)
            {
                return new AssignmentResult.NoWorkersAvailable();
            }
            var assignment = await assignmentRepository.Load(command.AssignmentId);
            if(!assignment.HasValue)
            {
                return new AssignmentResult.OtherError($"No assignment with id {command.AssignmentId} is available.");
            }

            assignment.Assign(worker);

            await assignmentRepository.Save(assignment);

            return new AssignmentResult.SuccessfullyAssigned(worker.Id);
        }
        catch (WorkerAlreadyAssigned ex)
        {
            return new AssignmentResult.WorkerAlreadyAssigned(worker.Id);
        }
        catch (WorkerNotQualifiedForAssignment ex)
        {
            return new AssignmentResult.WorkerNotQualified(worker.Id);
        }
        catch (Exception ex)
        {
            return new AssignmentResult.OtherError(ex.Message);
        }
    }
}

(not sure if you noticed, dear Reader, but I used a defensive, "functional" way of using Maybe type - if you are interested in this topic, please check Map, don't ask)

Phew, that was a bit exhaustive, but we managed to reach our goal!

We check our behavior specifications, also known as tests - it's all green ✅ (we have tests, right?)

We are using C#, object-oriented language, so this "object" (ok, I know, it's a record, but bear with me) does all the processing.

One observation might be that it's violating CQRS as it returns something, and we all know that it's either state changing or answering about state, never both, right?.

But is it really an answer? Or maybe an acknowledgement?

So our little "object" provides processing result, a value related to state of the processing, not the state of the system.

And how this processing result is consumed? As it is returned from the command handler Handle method, the command handler does not give a damn how it's used.

For command handler, it's transparent, context-free.

Let's look closely on the signature of the method:

public Task<AssignmentResult> Handle(AssignWorkerCommand command);

As you might recall dear Reader, we explored similar scenario in I, interface, where we changed the sequence of how inputs and outputs appear in the signature, creating a function:

let Handle = AssignWorkerCommand => Task<AssignmentResult>

This is how a function signature could look like in F#.

One could say that our command handler with its Handle method resembles a function - it accepts inputs, it uses capabilities provided by other objects, it returns a processing result as an output.

This is one of the most common approaches to handling commands - kind of a functional-style.

The way functions deliver "answers" is by returning a value.

What about objects?

"Telling" a result

One of the core aspects of object-orientation (which is a misnomer on its own; if you are interested in that topic - explore The ambiguity of objects) is sending and receiving messages.

Pointing at great, wise words by Sandi Metz:

Conclusion 🔍

You don't send messages because you have objects, you have objects because you send messages.

Let's ask ourselves - is AssignWorkerCommandHandler sending messages?

In a way, yes - it asks other objects to provide answers and based on those answers it decides what to do next.

It also receives messages - actually one message - AssignWorkerCommand message.

Turns out that message-passing collaborators, which can be another name for "objects", could be more fond of telling others what to do, rather than returning anything.

Message-passing is the key component here.

And this is exactly what Grzegorz presented with his solution.

It's a pure "Tell, Don't Ask" approach for designing interactions between objects - which means that in order to deliver processing results, objects need other objects to fullfil such responsibility.

If we look closely, one of the responsibilties our command handler has is deliverying "an acknowledgement", processing outcome.

What remains the same - deliverying results, how might vary.

One of the possibilities is returning the result so that downstream consumers can decide what to do.

Another one is closely related to "reporting results back" by a specialized role - "a result collector" (if you want to understand more about roles, please check The ambiguity of objects and The ambiguity of intefaces).

Shall we try using "Tell, Don't Ask" in this little problem space?

We need to change our command handler Handle signature, from this:

public Task<AssignmentResult> Handle(AssignWorkerCommand command);

to that:

public Task Handle(AssignWorkerCommand command);

"But where is the result?!", an avid Reader might be wondering.

It will come, no worries.

Responsibilities does not disappear, they are just assigned to other roles.

Now let's move to our HTTP interaction handler:

app.MapPost("/assignments/{assignmentId:guid}/assign",
async (Guid assignmentId, ICreateCommandHandler commandHandlerFactory) =>
{
    var resultCollector = new WorkerAssignmentResult();
    var commandHandler = commandHandlerFactory.Create<AssignWorkerCommandHandler>(resultCollector);
    await commandHandler.Handle(new AssignWorkerCommand(assignmentId));

    return resultCollector.Match(
        successfullyAssigned: assignmentResult => Results.Ok(new { assignedWorkerId = assignmentResult.WorkerId }),
        noWorkersAvailable: () => Results.UnprocessableEntity(new { message = "No workers available" }),
        alreadyAssigned: assignmentResult => Results.Conflict(new { message = "Assignment already has assigned worker", error = "assignments:assigning:no-workers-available", alreadyWorkerId = assignmentResult.WorkerId }),
        notQualified: assignmentResult => Results.UnprocessableEntity(new { message = "Worker is not qualified to do this assignment", error = "assignments:assigning:worker-not-qualified", unqualifiedWorkerId = assignmentResult.WorkerId }),
        otherError: message => Results.UnprocessableEntity(new { message = message; error = "assignments:assigning:other-error"})
    );
});

Note dear Reader that it is one of the possible implementations and it varies from what Grzegorz suggested.

Ok, let's get back to our command handler!

public record AssignWorkerCommandHandler
    (
        IProvideWorker workerProvider,
        IAssignmentRepository assignmentRepository,
        ICollectAssignmentResult resultCollector // 👈 new collaborator!
    )
    : ICommandHandler<AssignWorkerCommand, AssignmentResult>
{
    public async Task Handle(AssignWorkerCommand command)
    {
        try
        {
            var worker = await workerProvider.ProvideWorker();
            if(!worker.HasValue)
            {
                //👇🏻 same place but different way of achieving reporting result back!
                resultCollector.noWorkersAvailable();
                return;
            }
            var assignment = await assignmentRepository.Load(command.AssignmentId);
            if(!assignment.HasValue)
            {
                //👇🏻 same place but different way of achieving reporting result back!
                resultCollector.otherError($"No assignment with id {command.AssignmentId} is available.");
                return;
            }

            assignment.Assign(worker);

            await assignmentRepository.Save(assignment);

            //👇🏻 same place but different way of achieving reporting result back!
            resultCollector.successfullyAssigned(worker.Id);
            return;
        }
        catch (WorkerAlreadyAssigned ex)
        {
            //👇🏻 same place but different way of achieving reporting result back!
            resultCollector.workerAlreadyAssigned(worker.Id);
            return;
        }
        catch (WorkerNotQualifiedForAssignment ex)
        {
            //👇🏻 same place but different way of achieving reporting result back!
            resultCollector.workerNotQualified(worker.Id);
            return;
        }
        catch (Exception ex)
        {
            //👇🏻 same place but different way of achieving reporting result back!
            resultCollector.otherError(ex.Message);
            return;
        }
    }
}

This implementation of results collector is capable of being told what to do (a specific reporting) and provide answers when someone asks (using Match method).

So in a way it behaves both as an object (receiving messages) and a value object (that can be "mapped" into something else).

Now, command handler tells to resultCollector, being responsible for collecting a result from assignment processing, what to report.

AssignWorkerCommandHandler knows what happens when, and a someone who plays a role (that is able to report things back) will make it happen.

We saw WorkerAssignmentResult in action, so let's see how does he fullfil his responsibilities:

public interface ICollectAssignmentResult
{
    public void noWorkersAvailable();
    public void successfullyAssigned(Guid workerId);
    public void workerAlreadyAssigned(Guid workerId);
    public void workerNotQualified(Guid workerId);
    public void otherError(string message);
}

public class WorkerAssignmentResult : ICollectAssignmentResult
{
    private AssignmentResult _result; // 👈 we reuse the value object we have already brought earlier

    public void noWorkersAvailable()
    {
        result = new AssignmentResult.NoWorkersAvailable();
    }
    public void successfullyAssigned(Guid workerId)
    {
        result = new AssignmentResult.SuccessfullyAssigned(workerId);
    }
    public void workerAlreadyAssigned(Guid workerId)
    {
        result = new AssignmentResult.WorkerAlreadyAssigned(workerId);
    }
    public void workerNotQualified(Guid workerId)
    {
        result = new AssignmentResult.WorkerNotQualified(workerId);
    }
    public void otherError(string message)
    {
        result = new AssignmentResult.OtherError(message);
    }

    return TResult Match<TReturn>(
            Func<AssignmentResult, TReturn> successfullyAssigned,
            Func<TReturn> noWorkersAvailable,
            Func<AssignmentResult, TReturn> alreadyAssigned,
            Func<AssignmentResult, TReturn> notQualified,
            Func<string, TReturn> otherError
        ) => _result.Match(
            successfullyAssigned,
            noWorkersAvailable,
            alreadyAssigned,
            notQualified,
            otherError
        );
}

For the AssignWorkerCommandHandler it is transparent and unknown who satisfies "reporting capability".

It could be a dedicated object (WorkerAssignmentResult), a controller (as in Grzegorz's solution), or any other object that is able to respond to a call (response-ability, right?).

We've made a "little shortcut" and reused AssignmentResult value object from previous example, but still - wouldn't it be neat to have it?

Getting back to our command handler - it is a truly message-passing collaborator - it plays a major role in interactions with other objects.

If again look closely into Handle signature - it changed.

From types perspective, we don't have AssignWorkerCommand => Task<AssignmentResult>, but AssignWorkerCommand => Task instead!

Which initially might seem counterintuitive and against the natural way of thinking that people have, in which we tend to expect things to "flow" from top, to bottom, from left to right.

And here we throw things into void?

Do you know, dear Reader, what is the best way of checking the quality of our designs?

By introducing a change and see what is left when things are collapsing.

#1 Change, change never changes - "functional" command handler

Imagine that we were challenged to implement logging for each not-so-happy-scenario in assigning worker use case.

How does this affect our "functional" command handler?

public record AssignWorkerCommandHandler(
    ILogger<AssignWorkerCommandHandler> logger, // 👈 new collaborator!
    IProvideWorker workerProvider,
    IAssignmentRepository assignmentRepository
)
    : ICommandHandler<AssignWorkerCommand, AssignmentResult>
{
    public async Task<AssignmentResult> Handle(AssignWorkerCommand command)
    {
        try
        {
            var worker = await workerProvider.ProvideWorker();
            if(!worker.HasValue)
            {
                //👇🏻 same place as before for adding new behavior
                logger.LogError($"No workers available when trynig to assign workers for assignment {command.AssignmentId}");
                return new AssignmentResult.NoWorkersAvailable();
            }
            var assignment = await assignmentRepository.Load(command.AssignmentId);
            if(!assignment.HasValue)
            {
                //👇🏻 same place as before for adding new behavior
                logger.LogError($"No assignment with id {command.AssignmentId}");
                return new AssignmentResult.OtherError($"No assignment with id {command.AssignmentId} is available.");
            }

            assignment.Assign(worker);

            await assignmentRepository.Save(assignment);

            return new AssignmentResult.SuccessfullyAssigned(worker.Id);
        }
        catch (WorkerAlreadyAssigned ex)
        {
            //👇🏻 same place as before for adding new behavior
            logger.LogError($"Worker already assigned to assignment {command.AssignmentId}");
            return new AssignmentResult.WorkerAlreadyAssigned(worker.Id);
        }
        catch (WorkerNotQualifiedForAssignment ex)
        {
            //👇🏻 same place as before for adding new behavior
            logger.LogError($"Worker not qualified for assigning to assignment {command.AssignmentId}");
            return new AssignmentResult.WorkerNotQualified(worker.Id);
        }
        catch (Exception ex)
        {
            //👇🏻 same place as before for adding new behavior
            logger.LogError(ex, $"Other error happened when assigning worker to assignment {command.AssignmentId}");
            return new AssignmentResult.OtherError(ex.Message);
        }
    }
}

As a result, we also need to adjust a bunch of tests, as we changed not only the Handle method, but also a constructor.

"You could add this logging to controller, dumbass" - one might think.

Of course, we could assign the responsibilty for reporting logs to controller. Then we could report only failure scenarios there.

No doubts there.

But still we would need to add logging to only failure branches of the match, isn't it?

Ok, let's move on.

How will message-passing, collaborative command handler handle that requirement?

#1 Change, change never changes - message-passing, collaborative command handler

Of course it vastly depends on the implementation - but in fact it might not change at all!

We need to adjust one of its collaborators - someone who plays a role having the responsibility for collecting assigning results - ICollectAssignmentResult.

public class WorkerAssignmentResult(
    ILogger<AssignWorkerCommandHandler> logger
    Guid assignmentId,
) : ICollectAssignmentResult
{
    private AssignmentResult _result;

    public void noWorkersAvailable()
    {
        //👇🏻 same place as before for adding new behavior
        logger.LogError($"No workers available when trynig to assign workers for assignment {command.AssignmentId}");
        result = new AssignmentResult.NoWorkersAvailable();
    }
    public void successfullyAssigned(Guid workerId)
    {
        // no need to log here!
        result = new AssignmentResult.SuccessfullyAssigned(workerId);
    }
    public void workerAlreadyAssigned(Guid workerId)
    {
        //👇🏻 same place as before for adding new behavior
        logger.LogError($"Worker already assigned to assignment {assignmentId}");
        result = new AssignmentResult.WorkerAlreadyAssigned(workerId);
    }
    public void workerNotQualified(Guid workerId)
    {
        //👇🏻 same place as before for adding new behavior
        logger.LogError($"Worker not qualified for assigning to assignment {assignmentId}");
        result = new AssignmentResult.WorkerNotQualified(workerId);
    }
    public void otherError(string message)
    {
        //👇🏻 same place as before for adding new behavior
        logger.LogError($"Other error happened when assigning worker to assignment {assignmentId}. Message: {message}");
        result = new AssignmentResult.OtherError(message);
    }

    // `Match` method did not change
}

This causes a ripple during the HTTP interaction handling, let's fix that:

app.MapPost("/assignments/{assignmentId:guid}/assign",
async (ILogger<AssignWorkerCommandHandler> commandHandlingLogger, Guid assignmentId, ICreateCommandHandler commandHandlerFactory) =>
{
    var resultCollector = new WorkerAssignmentResult(commandHandlingLogger, assignmentId); // 👈 we had compile-time error here, it was an easy fix.
    var commandHandler = commandHandlerFactory.Create<AssignWorkerCommandHandler>(resultCollector);
    await commandHandler.Handle(new AssignWorkerCommand(assignmentId));

    return resultCollector.Match(
        successfullyAssigned: assignmentResult => Results.Ok(new { assignedWorkerId = assignmentResult.WorkerId }),
        noWorkersAvailable: () => Results.UnprocessableEntity(new { message = "No workers available" }),
        alreadyAssigned: assignmentResult => Results.Conflict(new { message = "Assignment already has assigned worker", error = "assignments:assigning:no-workers-available", alreadyWorkerId = assignmentResult.WorkerId }),
        notQualified: assignmentResult => Results.UnprocessableEntity(new { message = "Worker is not qualified to do this assignment", error = "assignments:assigning:worker-not-qualified", unqualifiedWorkerId = assignmentResult.WorkerId }),
        otherError: message => Results.UnprocessableEntity(new { message = message; error = "assignments:assigning:other-error"})
    );
});

As you probably noticed dear Reader - there is no magic, we need to touch some elements of our design.

This also might require adjusting some of the behavior specifications (tests), but on the different level.

Question is what we already alter - "logging" is pretty frequently happening capability, so it might have been added from the day one.

Although, one of the the reasons behind "logging" might be "reporting results", and in this scenario it was a breeze to be added in collaboration-based solution.

Functional-based solution got a bit "cluttered" with logging.

Some people are used to such appearances, so it does not seem "strange".

From objects to actors

By clearly segregating responsibilities for processing and for collecting results, we achieved some interesting qualities.

When we moved from "functional" to "message-passing collaborator", our command handler become something different.

It was still able to deliver the same capabilities, although it required help from others, so true power emerged from the collaboration, not objects acting alone.

Composing behaviors around reporting capability was already baked in, due to objects interacting with each other.

Don't get me wrong, dear Reader, I am not bashing or complaining on functional programming, as I am really into it.

Turns out that our AssignWorkerCommandHandler started looking more as an actor, rather than "a function".

Look what a simple act of changing how processing results are reported changed the nature of the handler.

It did processing and sent messages to others.

As we explored in The ambiguity of objects:

Conclusion 🔍

Transformations and message passing are two sides of the same coin.

Some might comment that it got too boilerplate-y, less terse and less readable.

Well, maybe.

There are systems built on top of the actor-model and prove their reliability, robustness and abilities to respond to a change in a swift fashion.

Initially, it might feel strange to "report back" results, instead of returning them.

It's a matter of getting accustomed to it.

But look on the bright sight - what qualities such design gives us back?

Imagine what superpowers you might get when you truly embrace actor-model, e.g. by using Akka.NET.

Bonus: #2 Change, change never changes: notifying about successful processing

As a bonus, imagine that there is another requirement to incorporate into our system: whenever a worker got successfully assigned, we should notify interested parties.

It should be done by sending a message by utilizing a queue.

If you were constrainted to use a message-passing collaborators approach - how would you design it, dear Reader?