Skip to content
Matthew Cochran edited this page Oct 29, 2022 · 24 revisions

Applinate Microservice Architecture Overview

Applinate aims to provide a complete microservice development platform where most of the core building blocks are done for you.

Sneak Peak

With Applinate, you create a service endpoint, implement it, and call it in three easy steps. When you follow the conventions, all the wiring is done for you.

Here's how easy it is.

Step 1. Define your service endpoint.

Write a contract for a request and response (your service contract endpoint).

[Service(ServiceType.Orchestration)]
public sealed class MyRequest:IReturn<MyResponse>
{
  // payload here
}


public sealed class MyResponse:IHaveRequestStatus
{
    // payload here
}

Step 2. Implement your service.

[Service(ServiceType.Orchestration)]
internal sealed class MyService : IHandleRequest<MyRequest, MyResponse>
{
    public async Task<MyResponse> ExecuteAsync(
        MyCommand arg,
        CancellationToken cancellationToken = default)
    {
        // logic here

        // return MyResponse
    }
}

Step 3. Call your Service

var request = new MyRequest();
var response = await request.ExecuteAsync();

...That's it.

Now, let's dive into the weeds a bit.

Why Applinate?

I'm open-sourcing the complete suite of Applinate add-ons to give you the building blocks you need assemble your services and shift that infrastructure investment to your business logic and presentation. This way, you can completely avoid reinventing the microservice plumbing, which is usually the most complex and biggest part (80%+) of most code bases. This means you can move fast (outpacing your competitors) while keeping your options open for any future technology upgrades.

Applinate provides a prescriptive service taxonomy, tooling, and conventions to help you separate plumbing code from your business code. This means you

  • Help your junior engineers avoid getting lost in the weeds so they are more productive, you can scale your organization, and protect yourself from source code degredation.
  • Help your senior engineers focus solely on infrastructure so you foster an culture where your most senior resources are invested in making more junior resources productive so you can scale you organization.
  • Get a jump-start with reusable tools to ship faster.

Using Applinate microservice building blocks gives your team less complex plumbing to figure out and more time to focus on delivering value to your customers while cutting the cost and time it takes to develop new features.

Applinate does not lock you into any particular technology or tools. For example, you can use the Applinate.Microservice.Encryption.Abstractions and Applinate.Microservice.Encryption.Core nuget package to use our encryption libraries or plug in whatever encryption library you want just by implementing the encryption interfaces in Applinate.Microservice.Encryption.Abstractions.

Note: Structurally, Applinate uses a similar approach as the popular MediatR package for messaging, but with additional tools focused on microservice development to protect you from unnessasry plumbing details.

How? Tools.

Applinate builds on a solid foundation while providing you with reusable tools you can use from your business logic.

  • Applinate.Microservice.Foundation is the core library with common infrastructure elements needed to build your microservices.
  • Helper Libraries
    • Microservice Unit Test Helper (coming Soon)
    • Microservice harness (coming Soon)
    • Decision Tables (coming Soon)
  • Microservice Tools
    • Initialization (built into the foundation library)
    • PubSub (coming Soon)
    • Compression (coming Soon)
      • Applinate.Microservice.Compression.Abstractions is the interface for using compression in your microservices.
      • Applinate.Microservice.Compression.Core is the interface for using compression in your microservices.
    • Caching(coming Soon)
    • Configuration (coming Soon)
    • Encryption (coming Soon)
    • Scheduling (coming Soon)
    • Authentication (coming Soon)
    • Authorization (coming Soon)
    • Payments & Product Provisioning (coming Soon)
    • User Profiles (coming Soon)

Architectural Approach

Let's walk through the different aspects of the Applinate service taxonomy, or if you are itching to see how it works you can click here to jump right to the code samples.

Service Types

The broader system taxonomy consists of five main types of services (components). These are the reusable building blocks for the system.

  • Client
  • Orchestration
  • Calculation
  • Integration
  • Tools (build your own or use the Applinate toolset to save time)

Orchestration Services

  • Encapsulate the volatility of your use cases and workflow logic.
  • Provide the entry point for all calls into your business logic tier.

Calculation Services

  • Encapsulate the volatility of your reusable domain-specific reusable logic
  • Provide coarse reusable components specific to the business domain

Integration Services

  • Encapsulate the volatility of your physical storage and external services

Tool Services

  • Encapsulate the volatility of reusable logic not specific to any particular business domain (build your own or use the Applinate toolset)

Client Tool

  • Your client used to trigger and Orchestration or Query.

Service Type Coupling Restrictions

There is a strict heirachy of coupling enforced at run-time. Anything not listed below is not allowed and will throw an exception:

  • Client Tools—May Call
    • Tool services
    • Orchestration services
  • Orchestration services—May call
    • Tool services
    • Calculation services
    • Integration services
    • other Orchestrator services (but this should be avoided unless absolutely necessary)
  • Calculation services—May call
    • Tool services
    • Integration services
  • Tool services—May call
    • Tool services

Orchestration

With an Applinate system, your primary entry point for all use cases is an Orchestrator service. Your Client only calls Orchestrator services, and this initial call into your infrastructure code (logic tier) is where Applinate distinguishes between dispatched commands and RPC-style queries. Once you are 'inside' your logic tier, all services should use RPC-style requests (but you don't have to)

Applinate builds on the Command Query Responsibility Segregation (CQRS) pattern.

You make all Applinate service calls with a request having a defined response. You achieve the CQRS pattern by having a special type of request not to execute immediately. Instead, you dispatch commands for execution later.

Any Orchestrator use case where you change the state of your system (e.g., change data) should use a command pattern. This helps you dispatch state mutations so your system follows a temporally decoupled event-based paradigm; it can scale and be eventually consistent.


How it Works

Requests

All request messages implement the IReturn<T> interface. This decorator interface has no implementation and designates a class as a request.

public interface IReturn<TResult>{}

And, your response message class must implement IHaveRequestStatus

All request messages must also indicate the type of service they are handled by

  • Orchestrator
  • Calculator
  • Integrator

example:

[Service(ServiceType.Orchestration)]
public sealed class MyRequest:IReturn<MyResponse>
{
  // payload here
}

public sealed class MyResponse:IHaveRequestStatus
{
    // payload here
}

Next, you must define an executor for processing each request. They must have a Service attribute and implement IExecuteCommand<TArg, TResult>.

Note: We recommend separating your interfaces and implementation into different assemblies to keep your code base healthy (it's better for you long-term but is not required). Request handler classes can live in implementation assemblies with no public classes to help you avoid quality degradation, coupling, and long-term maintenance issues.

[Service(ServiceType.Orchestration)]
internal sealed class MyService : IHandleRequest<MyCommand, MyResponse>
{
    public async Task<MyCommandResult> ExecuteAsync(
        MyCommand arg,
        CancellationToken cancellationToken = default)
    {
        // logic here

        // return MyCommandResult
    }
}

Applinate automatically wires your commands with the appropriate executor. There is nothing else you need to do except follow the Applinate convention.

Here's an example of how you execute a request:

namespace MyNamespace
{
    using Applinate;

    public static class MyClass
    {
        public async Task MyMethod()
        {
            var myCommand = new MyCommand();

            var myCommandResponse = await myCommand.ExecuteAsync();

            // logic using myCommandResponse

        }
    }
}

Dispatched Commands

Dispatched requests are used when you need to orchestrate a use case that will mutate data so that it can be executed in a separate process (think scalability). The immediate return value DispatchedCommandResponse just indicates if your command was successfully dispatched, but does not tell you anything about the execution. Dispatched commands is different from RPC queries that return results in the same call.

note: Applinate Orchestration services are the only recipients of dispatched commands used to execute use cases that mutate state. However, dispatched commands can also be used to implement more advanced choreography patterns for long-running operations like the saga pattern. However, we recommend trying to keep your system simple because the Saga pattern adds considerable complexity, and maintenance overhead, and makes it harder to rationalize about your system.

Dispatched commands are defined by commands that implement ICommand.

note: ICommand is just a special case if IReturn<TResponse>

public interface ICommand:IReturn<CommandResponse>{ }

example

[Service(ServiceType.Orchestration)]
public sealed record MyDispatchedCommand : ICommand
{
    // payload
}

public static class MyClass
{
    public async Task MyMethod()
    {
        var myCommand = new MyDispatchedCommand();

        var myDispatchResponse = await myCommand.ExecuteAsync();

        // post-dispatch logic 

    }
}

note: Applinate provides service event notifications so you can monitor request execution.


Request Interception

To help align with the OCP (Open Closed Principle) and SRP (Single Responsibility Principle), there are two extensions to requests that allow interception, or decoration of their behavior.

Following the conventions, Applinate will automatically inject the behavior when you execute the request. There is nothing special you have to do to enable interception because interception is built into the request execution pipeline.


Basic Interception

Proxy interception is used when there are specific Command/Response pairs you want to add behavior to. In order to do this, you only have to create a class derived from InterceptorBase or CommandInterceptorBase and

Note: the Intercept attribute is optional, and used if you have multiple interceptors and need to specify the order of execution.

example

[Intercept(1)]
public class MyInterceptor : InterceptorBase<MyCommand, MyCommandResult>
{
    // this constructor must be present 
    public MyInterceptor(ExecuteDelegate<MyCommand, MyCommandResult> core) : base(core)
    {
    }

    // optional pre-processing override
    protected override Task<MyCommand> PreProces(MyCommand arg)
    {
        // replace or modify the command before execution

        var newArg = Modify(arg);        
        return base.PreProcess(newArg);
    }

    // optional post-processing override
    protected override Task<MyCommandResult> PostProces(MyCommandResult response)
    {
        // replace or modify the result after execution and continue
        var newResponse = Modify(response);
        return base.PostProcess(newResponse);
    }

    // optional complete override
    protected override async Task<MyTestCommandResult> Execute(MyCommand arg, CancellationToken cancellationToken)
    {
        // pre-processing

        // execute the base (or not)
        var result = await base.Execute(arg, cancellationToken);

        // post-processing

        return result;
    }
}

Interception Factory

If you want to intercept all commands and inject behavior across your entire system (every command/response pair) you can use an interception factory.

This is a less common use case, but extremely useful for broader aspects like logging or tracing.

To create an interception factory, you derive a class from InterceptorFactoryBase and override the ExecuteAsync<TArg, TResult>() method. Applinate will automatically wire up the request with the handler.

Note: the Intercept attribute is optional, and used if you have multiple interceptors and need to specify the execution order.

[Intercept(5)]
public class MyInterceptorFactory : InterceptorFactoryBase
{
    public override async Task<TResult> ExecuteAsync<TArg, TResult>(ExecuteDelegate<TArg, TResult> next, TArg arg, CancellationToken cancellationToken)
    {
        // pre-process (optional)
        var newArg = Update(arg);

        // execute (or not)
        vr result = await base.ExecuteAsync(next, newArg, cancellationToken);

        // post-process (optional)
        var newResult = Update(result);

        return newResult;
    }
}


System Taxonomy

The architecture consists of three core service types for your business logic. Applinate is intended to assist you in encapsulating change in your code base over time (aka volatility-based decomposition) by separating orchestration, calculation, and integration concerns.




IOC (without DI)

Dependency Injection (DI) is one way to implement the Inversion of Control (IOC) principle. However, it's not always the best way, so Applinate gives you the option to achieve IOC without DI.

While Applinate does not prevent you from using DI, to avoid constructor bloat, avoid unnecessary complexity, facilitate service testing, and reduce complexity, you can use tools through a provider model, which is Applinate's preferred method of achieving IOC.



Initialization

todo: add docs



Command Context

todo: add docs



Structural Conventions

Services should have an *.Abstractions assembly to surface their publicly available contracts.

All services have an implementation assembly, where all types are internal and provide the backing functionality for their abstractions.