Software Development

Local Microservices: First-Class Procedures

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

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 it’s 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 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:

What the First-Class Procedures allows is containerising 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 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

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

Published on Java Code Geeks with permission by Daniel Sagenschneider, partner at our JCG program. See the original article here: Local Microservices: First-Class Procedures

Opinions expressed by Java Code Geeks contributors are their own.

Daniel Sagenschneider

Founder of OfficeFloor and an evangelist for "true" inversion of control
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button