Writing Ports and Adapters for an Hexagonal Architecture


Domain Driven Design (DDD) is all about the domain. It is challenging on the number of abstractions which needs to occur to protect the domain at all costs; the concepts which is challenging to the programmer who comes from a layered n-tier application.

Micro-services seem to be the up and trending opinion on how applications should be built to deliver nimble, yet very powerful solutions to the business domain; which compliments the DDD mantra very well.

The technical principle from micro-services and DDD is simple: only focus on the properties of the supplied inputs and desired outputs of the process, and leave the implementation details to be filled in where and when required. So much so, DDD has a concept of Context Maps which has the sole purpose to determine what things go where, but not how those things are operated.

I have been doing a lot of thinking about applying that same principle into the Ports and Adapters when sending inputs to the the domain, and expecting a certain response. Some less desirable concerns always needed some level of implementation detail to be handled by the developers, such as the following list.

  1. Mixing asynchronous and synchronous operations.
  2. Converting between primitive types.
  3. The ability to compose new operations, without being required to build a whole new domain service.

Whilst thinking about the above problems, my appreciation for the UNIX philosophy grew. Simply pass stream into stream with each micro-program performing an operation so that larger programs could be wielded together.

First Skunk Works

My first stab at creating a pattern which reduces the upfront implementation concerns heavily relied on the Mediator pattern which would query the Dependency Injection framework for the details. From a high level, an IHandler<TInput, TOutput> interface was implemented by the various “micro-programs.”

We then ran into a few issues with the above approach.

  1. Application start-up was slow. Scanning all the assemblies for the relevant types were slow.
  2. There was a massive explosion of micro-classes within the subsystem.
  3. It never solved the mixing of asynchronous and synchronous code.

Introducing SemanticPipes 1.0

I tried to locate a library which would address the stated desired characteristics, but my search seemed to have zero results. Since it was an itch that I needed to solve, and explore the problem, I started to code SemanticPipes under the MIT license.

The SemanticPipes code base has reached a version which, in my view, is suitable for others to start adopting it in their own production environments. So the version number has ticked over to 1.0.

NuGet

Install-Package SemanticPipes

Quick Start Example

Below is an extract of the example which has been placed in the README file.

Here is a quick start guide in a form of a unit test. The code is written to be read from top down, and the comments describes what is being demonstrated.

Take note that the test will run in about 1 second, and not n*1sec where n=7 (parallel execution). This is because when the view model pipe scatters the various domain requests as a parallel task. Upon the gather of the List<PingServerDomainResponse>, an implicit gather is made. So how about that? Implied parallel processing loops without the programmer thinking about it.

And here is the code listing.

[TestFixture]
public class QuickStart
{
  /*
  *  These are the typical number of domains which exist
  *  within your DDD application for a certain command
  */

  public class PingServerCommandViewModel
  {
    public IEnumerable<int> Servers { get; set; }
  }

  public class PingServerResponseViewModel
  {
    public int NumberOfOnlineServers { get; set; }
    public int NumberOfOfflineServers { get; set; }
    public IEnumerable<int> OnlineServerInstanceIds { get; set; }
    public IEnumerable<int> OfflineServerInstanceIds { get; set; }
  }

  public class PingServerDomainCommand
  {
    public int ServerInstanceId { get; set; }
  }

  public class PingServerDomainResponse
  {
    public int ServerInstanceId { get; set; }
    public bool IsOnline { get; set; }
  }

  /*
  * So the incoming request and response are within the application edge.
  *
  * The broker parameter is injected via a DI framework for the UI components,
  * however, there is nothing stopping you from moving the instance access elsewhere.
  *
  * Also, take note of the Task being returned. Most modern application endpoints support
  * this kind of notation to assist in the number of concurrent requests that it would
  * be able to support. This is baked in
  */

  public Task<PingServerResponseViewModel> PingServers
  (ISemanticBroker broker, PingServerCommandViewModel command)
  {
    return broker.On(command).Output<PingServerResponseViewModel>();
  }

  /*
  * Imagine that this is your bootstraping components and creates
  * a global variable (or enrolls it into a DI framework).
  *
  * For this example, we are going to simulate a request in the method
  */

  [Test]
  public async Task BoostrappingStartup()
  {
    var builder = new SemanticBuilder();

    // this is where you will scan all the relevant components into
    // the SemanticBuilder
    RegisterPipeComponents(builder);

    // after that, capture this instance (thread-safe) somewhere so
    // that you are able to reuse it with other requests
    ISemanticBroker broker = builder.CreateBroker();

    // simulate a request/response
    PingServerResponseViewModel response = await SimulateRequestResponse(broker);

    Assert.AreEqual(3, response.NumberOfOfflineServers);
    Assert.AreEqual(4, response.NumberOfOnlineServers);
  }

  private Task<PingServerResponseViewModel> SimulateRequestResponse(ISemanticBroker broker)
  {
    var requestModel = new PingServerCommandViewModel
    {
      Servers = new[] {1, 4, 7, 10, 11, 12, 20}
    };

    return PingServers(broker, requestModel);
  }

  private void RegisterPipeComponents(SemanticBuilder builder)
  {
    // logically, in your application, the domain pipes and the
    // view model pipes will be scattered around the solution
    RegisterViewModelComponents(builder);
    RegisterDomainModelComponents(builder);
  }

  private static void RegisterViewModelComponents(SemanticBuilder builder)
  {
    // firstly, our domain model doesn't support bulk queries natively,
    // so we are going to send them multiple single commands (see the IEnumerable)
    builder.InstallPipe<PingServerCommandViewModel, IEnumerable<PingServerDomainCommand>>(
      (model, broker) =>
      model.Servers.Select(i => new PingServerDomainCommand {ServerInstanceId = i})
    );

    // since we know that we scatter the requests out, let's gather them in again
    // by inverting the IEnumerable (or this case, List - it doesn't matter)
    builder.InstallPipe<List<PingServerDomainResponse>, PingServerResponseViewModel>
    ((responses, broker) => new PingServerResponseViewModel()
    {
      NumberOfOfflineServers = responses.Count(x => !x.IsOnline),
      NumberOfOnlineServers = responses.Count(x => x.IsOnline),
      OfflineServerInstanceIds =
      responses.Where(x => !x.IsOnline).Select(x => x.ServerInstanceId),
      OnlineServerInstanceIds =
      responses.Where(x => x.IsOnline).Select(x => x.ServerInstanceId)
    });
  }

  private static void RegisterDomainModelComponents(SemanticBuilder builder)
  {
    // this is how the domain requests will come in, one by one.
    // notice that this pipe is executed asynchronously with the async keyword
    // being the compiler clue
    builder.InstallPipe<PingServerDomainCommand, PingServerDomainResponse>(
      async (command, broker) =>
      {
        // imagine this is a database query or actual ping.
        await Task.Delay(TimeSpan.FromSeconds(1));

        return new PingServerDomainResponse()
        {
          // spot the odd one out :)
          IsOnline = command.ServerInstanceId%2 == 0,
          ServerInstanceId = command.ServerInstanceId
        };
      });
  }
}
blog comments powered by Disqus