Skip to content

Instantly share code, notes, and snippets.

@jeremywall
Created August 2, 2024 19:17
Show Gist options
  • Save jeremywall/5e664772adc7fa8e340d352e68d71bcb to your computer and use it in GitHub Desktop.
Save jeremywall/5e664772adc7fa8e340d352e68d71bcb to your computer and use it in GitHub Desktop.
Java app layers for build up and tear down

I've never been a fan of frameworks for Java apps that are not web servers. I have always found them lacking in features related to organizing the application's internal architecture for non-web server applications. Every year or so I look around to see if that's changed, and I've still never found anything remotely like what I want amongst the big frameworks like Spring Boot, Quarkus, Micronaut, etc. My biggest complaint is the lack of clean build up and tear down mechanisms in these frameworks. What I have been using the last few years is Guava Services (description at https://github.com/google/guava/wiki/ServiceExplained).

Guava's Services let me create a tiered layer system. For example, starting lower level services before the higher level services that are dependent on the lower services. A concrete example is a layer that creates database connection pools is started before the layer that kicks off the business logic that requires access to the database connection pool. Or a layer that starts a Kafka or Amazon Kinesis publisher client is fired up and ready to use before the layer that will generate data to send to the publisher. I strive to use decoupled designs inside my applications and before I adopted the layered approach I have had intermittent race conditions where I was reading from a Kinesis stream, processing the data, and throwing a message in the application's internal event bus that was destined to be written to another Kinesis stream before the Kinesis publisher client had even started so the data was dropped on the floor. Using the Guava Services to create a layered start up system helps ensure the race conditions do not happen.

Here's an example of how I orchestrate start up and tear down of the layers using Guava Services. Each of my service implementations has start and stop functions invoked by the associated service manager as the manager is starting or stopping it's child services. It's in these start and stop functions that I create database connection pools, create a stream consumer, create a stream producer, etc. The services act as the access point to whatever they create so the RedisService contains a Redis client and if you need to access Redis you just dependency inject the RedisService into your code and then use it to interact with Redis via it's internal client.

In this example the StreamConsumerService doesn't start reading from the stream as it's started. The start of the service just creates the stream reading client object. It's the start of the final service, the StartService, that then uses the StreamConsumerService to initiate reading from the stream.

So after all this introduction and back story I'm back to my initial question. I've never see anything like this kind of tiered service process in any existing framework. Am I missing something? Is there something like this but I'm not finding it in Spring Boot, Quarkus, Micronaut, etc.?

import com.google.common.util.concurrent.ServiceManager;

public class App {
    // level 1 services
    @Inject private EventBusService eventBusService;    // extends AbstractIdleService
    @Inject private MetricsService metricsService;      // extends AbstractIdleService

    // level 2 services
    @Inject private mysqlService mysqlService;          // extends AbstractIdleService
    @Inject private RedisService redisService;          // extends AbstractIdleService

    // level 3 services
    @Inject private StreamPublisherService streamPublisherService;  // extends AbstractService

    // level 4 services
    @Inject private StreamConsumerService streamConsumerService;    // extends AbstractService
    
    // level 5 services
    @Inject private StartService startService;    // extends AbstractIdleService

    private ServiceManager serviceManagerLevel1;
    private ServiceManager serviceManagerLevel2;
    private ServiceManager serviceManagerLevel3;
    private ServiceManager serviceManagerLevel4;
    private ServiceManager serviceManagerLevel5;

    public static void main(String[] args) {
        // parse command line args
        // parse config file
        // build up dependency injection graph, currently Guice but have been migrating projects to Toothpick
        App app = guiceInjector.getInstance(App.class);
        app.go();
    }
    
    public void go() {
        // start level 1 and wait till all are up and healthy
        serviceManagerLevel1 = new ServiceManager(Arrays.asList(eventBusService, metricsService);
        serviceManagerLevel1.startAsync().awaitHealthy();

        // start level 2 and wait till all are up and healthy
        serviceManagerLevel2 = new ServiceManager(Arrays.asList(mysqlService, redisService);
        serviceManagerLevel2.startAsync().awaitHealthy();

        // start level 3 and wait till all are up and healthy
        serviceManagerLevel3 = new ServiceManager(Arrays.asList(streamPublisherService);
        serviceManagerLevel3.startAsync().awaitHealthy();
        
        // start level 4 and wait till all are up and healthy
        serviceManagerLevel4 = new ServiceManager(Arrays.asList(streamConsumerService);
        serviceManagerLevel4.startAsync().awaitHealthy();

        // start level 5 and wait till all are up and healthy
        serviceManagerLevel5 = new ServiceManager(Arrays.asList(startService);
        serviceManagerLevel5.startAsync().awaitHealthy();

        // block here until level 5 services are stopped
        // level 5 service manager stop initiated by code that intercepts SIGINT and SIGTERM signals
        serviceManagerLevel5.awaitStopped();

        // stop level 4 services and wait until stopped
        serviceManagerLevel4.stopAsync().awaitStopped();

        // stop level 3 services and wait until stopped
        serviceManagerLevel3.stopAsync().awaitStopped();

        // stop level 2 services and wait until stopped
        serviceManagerLevel2.stopAsync().awaitStopped();

        // stop level 1 services and wait until stopped
        serviceManagerLevel1.stopAsync().awaitStopped();

        System.exit();
    }
}
@kucharzyk
Copy link

I've never had such problems with any Java applications.
CDI/Spring should resolve all dependencies for you.

Suppose you have REST API which is constructed in standard way:

  • Rest resource is using service with business logic
  • Service is using repository
  • And repository is using data source

You don't have to do anything to make it working

  • Rest resource will be created after service with business logic
  • Service will be created after repository
  • And repository will be created after data source

Bean creation order is always correct because container is resolving all dependency graph.

Is is something I am missing about your use case?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment