Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Local Microservices: First-Class Procedures

DZone 's Guide to

Local Microservices: First-Class Procedures

In this post, a microservices expert considers local (pass by reference) microservices and their impact on development.

Free Resource

This is the third article in a three part series on local microservices. The first two articles looked at:

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:

  1. Object references are a nice graph of nodes (objects) and lines (fields).
  2. Object methods have a significant coupling problem creating a jigsaw of behavior.
  3. Microservices break the method couple to return behaviour to a graph of nodes (microservices) and lines (HTTP requests/Queue messages).

There is an underlying pattern to representing this decoupled behavior.  It is HTTP URL/Queue name and payload/message type. This decoupled client calling pattern can be represented with the following general interface:

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 are 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.

So can we have the microservice called and executed by the same thread, and still continue to provide the microservice advantages of bounded contexts (in other words, the smaller jigsaws)?

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 its fields are no longer necessary. The above procedure links the required objects together by being parameters.

This execution is now:

  1.  ClientCall used to invoke a procedure.
  2. Procedure pulls in appropriate dependencies.
  3. 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 is, 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:

The First-Class Procedures allow for the containerizing of small snippets of logic within a procedure. These procedures communicate via loosely coupled continuations that require only a single object (payload message). The remaining objects are dependency injected. Furthermore, threading models can be specific to each procedure.

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.

The difference is that the same thread can call and execute the First-Class Procedure. In other words, First-Class Procedures run locally with each other.

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 a 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 out that you got the microservice mix wrong involves a lot of effort to fix it.

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 it. First-class procedures are weaved together graphically. Therefore, to change the mixture we simply rewrite the procedures and then draw 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 base, 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 an 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.

The above is not an exhaustive list.

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 buy other companies, capacity limits on in-house data centers, 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 money spent. 

Summary

For me, local microservices (i.e. pass by reference mircoservices) are 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.

Topics:
microservice components ,microservices adoption ,microservices ,first-class procedure ,inversion of control ,microservices tutorial ,microservices tutorial java

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}