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();
}
}
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:
You don't have to do anything to make it working
Bean creation order is always correct because container is resolving all dependency graph.
Is is something I am missing about your use case?