These articles identified the method coupling of Object Orientation creates a monolithic jigsaw of different shaped objects. Microservices are breaking these into more manageable, smaller jigsaws that appear similar in shape.
This article continues the breaking down to consider local (pass by reference) microservices.
Part Three: Local Microservices via First-Class Procedures
The first two articles in this series identified:- Object references are a nice graph of nodes (objects) and lines (fields)
- Object methods have a significant coupling problem creating a jigsaw of behaviour
- Microservices break the method couple to return behaviour to a graph of nodes (microservices) and lines (HTTP requests, / Queue messages)
interface ClientCall<T> {
void invokeService(T singleObject);
}
This client calling interface is then implemented by the appropriate HTTP request service(...) method or Queue onMessage(...) method. These methods are usually found on the following objects:
public void SomeHttpServicerImpl {
@Inject SomeRepository someRepository;
@Inject AnotherRepository anotherRepository;
@Inject ClientCall<SomeArgument> anotherMicroservice;
// other dependencies
public void service(SomeObject httpRequestEntity) {
// service HTTP request with injected dependencies
}
}
public void SomeQueueConsumerImpl {
@Inject SomeRepository someRepository;
@Inject AnotherRepository anotherRepository;
@Inject ClientCall<SomeArgument> anotherMicroservice;
// other dependencies
public void onMessage(SomeQueueMessage message) {
// service Queue message with injected dependencies
}
}
Furthermore, what is not shown clearly is the threading model. As the HTTP servicer or Queue consumer are in their own process, they are run with their own threads.
The result is the following pattern for implementing the microservice:
- Single object provided by client
- Remaining objects are dependency injected
- Thread used is based on service/consumer implementation
- Interaction with other microservices is via single parameter ClientCall
The issue with this pattern is that all calls to other microservices require the microservice to be executed by another thread. As the mciroservice resides behind HTTP requests / Queues, there is process boundaries preventing the calling thread from executing the microservice.
The process boundary separation provides a bounded context, so that the microservices are isolated from each other. However, this separation puts a lot of communication overheads and network error handling into microservice solutions. Plus it disallows microservices from being executed by the same thread.
Local Bounded Context
To see how local (same thread calling/executing) microservices can be achieved, we need to transform the above implementations slightly.Rather than field/setter injection, let's look at using constructor injection. We could turn the above implementation into the following:
public void SomeMicroserviceImpl {
private final SomeRepository someRepository;
private final AnotherRepository anotherRepository;
private final ClientCall<SomeArgument> anotherMicroservice;
@Inject
public SomeMicroserviceImpl(
SomeRepository someRepository,
AnotherRepository anotherRepository,
ClientCall<SomeArgument> anotherMicroservice) {
this.someRepository = someRepository;
this.anotherRepository = anotherRepository;
this.anotherMicroservice = anotherMicroservice;
}
public void service(SomeObject httpRequestEntity) {
// service HTTP request with injected dependencies
}
}
However, that's a lot of code!
Rather, why not just inject the dependencies directly into the method:
public static void service(
SomeObject httpRequestEntity,
SomeRepository someRepository,
AnotherRepository anotherRepository,
ClientCall<SomeArgument> anotherMicroservice) {
// service HTTP request with injected dependencies
}
The method has effectively become a procedure. The object and all it's fields are no longer necessary. The above procedure links the required objects together by being parameters.
This execution is now:
- ClientCall used to invoke a procedure
- Procedure pulls in appropriate dependencies
- Procedure then invokes other procedures via the ClientCall interface
The execution is no longer methods navigating the Object references, locking you into monolithic jigsaw. It is now procedures invoking each other, pulling in only the required dependencies for the procedure.
As the procedure pulls in only its required objects, it provides a bounded context. One procedure may pull in a particular set of objects, while another procedure may pull in a totally different set of objects. As the procedure joins the objects, we no longer have to create a big graph of all objects referencing each other. We can separate the objects into smaller graphs. This break down allows separation of objects into bounded contexts.
Now the question comes of how can we implement this so the procedures run within the same process space?
First-Class Procedure
Well this procedure is remarkably similar to the First-Class Procedure. See:- OO Functional Imperative Reactive weaved together (for a running code example)
- Function IoC for First Class Procedure (for more of the theory and how the containerising works)
The two bounded context approaches have similar characteristics:
- HTTP/Queue communication can be considered a single argument Continuation
- Threading models can be different within each first-class procedure / microservice process
- Dependency Injection of both allows access to only the required object graph allowing smaller object jigsaw puzzles (no monoliths). In other words, bounded contexts.
Remote vs Local
But don't microservices want to be process separated to allow different release cycles and scalability?Yes, that is absolutely true once up and running in production with heavy load of users. However, what about getting started with microservices?
For me this falls into the problem of being opinionated too early. To get the right mix of microservices takes a significant amount of requirements gathering and architecture. Why? Because refactoring microservice architectures can be expensive. Microservices involve a lot of overhead in typically different code repositories, build pipelines, network failure handling, etc. Finding you got the microservice mix wrong involves a lot of effort to change.
By starting out with first-class procedures, you get to try a local microservice mix. If the mixture is wrong, it is very quick to change them. First-class procedures are weaved together graphically. Therefore, to change the mixture is simply rewriting the procedures and then drawing the new connections between them. Yep, that's it. No code moving between repositories. No changing build pipelines. No extra error handling because of network failures. You can get on with trying out various mixes of local microservices (first-class procedures) all on your local development machine.
Once you find a mix you are happy with, deploy all of them in the one container. Why? Because unless you have a large user based, you can run your first-class procedures in just one node (possibly two for redundancy). Having less deployed nodes, means less cloud instances. Less cloud instances, is well less dollars.
Then as your load increases, you can split out the first-class procedures into separate nodes. Just change the continuation link between them to either a HTTP call or Queue. Furthermore, this split can then be for various reasons you may discover along the way:
- differing functionality change cycles
- differing team responsibilities (e.g. Conway's Law)
- data governance may mean geographic restrictions
- security may require some to run on premise
- on premise capacity limits may mean pushing some to public clouds
Having to requirements gather and architect the microservice mix given all of the above could get quite exhausting. Especially, as some aspects are quite fluid (e.g. teams change, companies buying other companies, capacity limits on in house data centres, etc). There are significant factors making it difficult to find the right mix of microservices up front.
Plus this also works in reverse. As things change and some aspects don't experience higher loads or significant functional changes, they can be combined back into single instances. This reduces the number of cloud instances required, and again reduces dollars.
Summary
For me, local microservices (i.e. pass by reference mircoservices) is going to eventuate. This is similar to session EJBs being introduced because the EJB 1.0 specification of only remote calls was just too heavy. Yes, we have better infrastructure and networks than 20 years ago. However, the financial overhead costs of only remote microservices may soon be considered heavy weight and expensive given that local (pass by reference) First-Class Procedures are available.So if you are finding the "remote" microservice architect a heavy weight and expensive consideration, try out First-Class Procedures as a local microservice solution. This gets you many of the benefits of microservices without the expense. Then, as your load increases and your revenues increase, scale into a remote microservice architecture. But this is only where you see real need, so you can keep your infrastructure costs down.