Decide between In-Process or Inter-Process Communication at Deploy Time - Part 2
This post continues the previous by giving an example on how we could implement a design that allows to decide only at deploy time how services communicate: in process if they are deployed on the same server or inter-process if they are on different machines. If you haven't read the previous post, you should go through it to get the context and where such a design is useful. Now, we are going to implement it for a simple example.
As stated previously there are three key design ideas we're going to put into practice:
- Depend only on Contracts, which are expressed by abstract types (interfaces)
- Use Proxies to forward the call to a contract to the actual implementation
- Use Type Discovery to determine what implementations were deployed on each process
Let's pick an example inspired from a financial system. We need some services that depend one on the other, so we have some communication to play with. Say we have a PortfolioService
which can get the current value of a portfolio, then we have an OrdersService
to place buy or sell orders and we also have a QuotationService
for getting quotations. The PortfolioService
and the OrdersService
call (depend on) the QuotationService
, like in below diagram
To keep things simple, we say that each of these services is implemented by one class. Notice, that each of the classes in the diagram implement a correspondent interface, which represents the contract of the service.
In one deployment we would want that OrdersService
directly calls (in the same process as a function call) the QuotationService
(the blue arrow) and the PortfolioService
makes a inter-process communication to call the QuotationService
(green arrow). Then, without changing any code we'd want to change this and have all to use inter-process communication or all in-process communication.
Depend on Contracts
Another important thing to notice is that the PortfolioService
and the OrdersService
depend on the interface IQuotationService
and not on the implementation class. This is an important constraint that our design employs.
To enforce this constraint we will place all the public contracts in a separate assembly (Contracts
) which will only have contracts and which will be available on all the servers where we'll deploy. This Contracts
assembly may be referenced by any service implementation from any module. This allows any service to consume any service, but it will depend on its contract and not on its implementation. In fact it will not know if it will talk to the actual implementation or to a proxy which forwards its call to another process on a different server. The IQuotationService
contract will be like:
public interface IQuotationService
{
Quotation[] GetQuotations(string exchange, string instrument, DateTime from, DateTime to);
Quotation[] GetQuotations(string securityCode, DateTime from, DateTime to);
}
public class Quotation
{
public DateTime Timestamp { get; set; }
public decimal BidPrice { get; set; }
public decimal AskPrice { get; set; }
public string SecurityCode { get; set; }
}
### Generic Process Host
An important building block of this design is to have a process which is able to host any of the services we may have. It should be able to:
- host one or more services
- publish these services to be consumed from another processes on another server
- discover at startup which services were deployed, host and publish them
In our simple example we will build it as a console app and we'll publish the services as REST endpoints. In a real distributed application on Azure this may be a Cloud Service or it could leverage the benefits of the Azure Service Fabric. On premises it may be a Windows Service.
Now in a deployment like this
where we have each service hosted alone, in its own process, they will all do inter-process communication. In a real system, such a deployment where everything is self hosted may be the first one we try out. It maximizes scalability. Then based on collected metrics we may group things to reduce the communication overhead.
For our example, let's assume that we observe that the OrdersService
and the QuotationService
have a very intense communication and that the communication overhead is significant. We can deploy a copy of the QuotationService
in the same place with the OrdersService
and load them in the same host like this
Now these two will communicate in the same process, only through function calls, and the PortfolioService
will continue to use inter-process calls to a QuotationService
instance that remained hosted individually.
Even more, for testing purposes maybe, we could deploy all the services in the same place and load them all in the same process like below.
We can make all these new deployments, without making any code change.
Solution Structure
The way we organize the code in a Visual Studio solution (or into folders) and how we allow references to be created is a critical aspect of this design. We want to gain a huge flexibility at deployment, so we need loose, well managed and controlled dependencies. The folders structure should lay this out. Also we should have clear rules on which assembly can be referenced by whom.
Here is a view of the structure for our example.
The first level of separation is outlined by the root folders: Infrastructure
and Modules
.
The Infrastructure
will contain code that has nothing to do with the functional use-cases and the business logic of the application. The code here is to implement the non-functional requirements and to support the implementation of the business logic. Here we have the Proxies
, the utilities for the host process and some extensions for the AppBoot
.
The Modules
folder, on the other hand, contains the implementation of the functional use cases. Here is where the business logic is. Within it, we have a second level of separation: the functional modules, each represented by its own folder. We also have here the Contracts
assembly which has the functional contracts among the modules, again separated in its own folders and namespaces.
If we look at the references or dependencies in below diagram,
we see that we don't have nor allow references between the modules. This is important if we want to be able to deploy them in separate processes. They all depend on the Contracts
, and they may also depend on the Infrastucture
. The Contracts
and the Infrastructure
binaries will be deployed and available on all the servers where we deploy.
Proxies
The proxies are another key element of this design. Each interface from the Contacts
assemblies will have at least two types of implementations: the real implementation and the proxies implementations. The proxies will just forward the call to the real implementation.
In our example we use REST for inter-process communication, so the proxy will create a HttpClient
, will call the REST endpoint and will return the result. Here's an example for the QuotationService REST Proxy:
class QuotationServiceProxy : IQuotationService
{
public Quotation[] GetQuotations(string exchange, string instrument, DateTime @from, DateTime to)
{
using (HttpClient client = HttpHelpers.CreateNewClient<IQuotationService>())
{
string path = HttpHelpers.GetServicePath<IQuotationService>("GetByExchange");
string uri = $"{path}?exchange={exchange}&instrument={instrument}&from={from}&to={to}";
HttpResponseMessage response = client.GetAsync(uri).Result;
if (response.IsSuccessStatusCode)
{
Quotation[] value = response.Content.ReadAsAsync<Quotation[]>().Result;
return value;
}
throw new HttpException((int)response.StatusCode, response.Content.ReadAsStringAsync().Result);
}
}
...
We should have at least two types of proxies:
- Inter-Process Proxies, which forward the call to another process using an inter-process communication protocol (like the one in the above example), and
- In-Process Proxies, which forward the call to the real implementation in the same process (basically this is just a wrapper over the real implementation)
In our implementation example, we will skip the In-Process Proxies and use directly the real implementation. However, in a real application it is needed because we need to have a consistent contract implementation from the caller perspective, no matter if it calls in process or inter-process.
For example, let's assume that the real implementation of the IQuotationService.GetQuotations()
throws under certain conditions (a bug maybe) IndexOutOfBoundsException
. If it is called in the same process directly, the caller will get this exception. At the same time, if it is called through the Inter-Process Proxy the caller will get an HttpException
. This is not good. The caller calls an interface and we want it not to care if it calls in process or inter-process. In this example the fault contract is not consistent.
So the proxies should wrap the implementation and make sure that the public contracts in the Contracts
assembly are consistently implemented for both in-process and inter-process calls.
Type Discovery
Having only dependencies against interfaces, Dependency Injection is a handy technique to get this duality on using an In-Process Proxy or an Inter-Process Proxy to get to the real implementation of a service we depend on.
At startup, before it is ready to receive requests, the host process (the ConsoleHost
in our example) will scan all the deployed binaries in its output folder to determine which services were deployed and which of them should be published to be called from other processes. Using conventions, it will do the proper configuration of the Dependency Injection Container.
For example if for a contract it has the real implementation deployed it will register into the container a In-Process Proxy that will call it. If it doesn't have the real implementation it will register the Inter-Process Proxy, which will know haw to forward the call through HTTP, in our case to the service implementation from another process.
Also discovering which services are deployed, the host process may use some other conventions to know which to publish get calls from other processes. The easiest convention is to publish all deployed services.
Summary
As a continuation of the previous post, we have defined an example to show how to implement this design. We have detailed the key building blocks on it: Depend on Contracts, Generic Process Host, Solution Structure, Proxies and Type Discovery. Now we should have a good idea on how to implement this.
In the next post, we will continue with the implementation, focusing on the code of the ConsoleHost
and the Proxies
to get to an example that we can run and with which we can have different deployments for these three services without changing their code.