Java beyond Spring: A set of alternatives.

Ignacio Cicero
19 min readApr 13, 2021

Have you noticed that everywhere you could work as a Java developer, there is a 90% chance of Spring -and specially Spring-boot- being the library of choice, regardless of the business, company size, etc?

I was thinking that there has got to be a reason for it. Everyone uses it, the community is huge, you do not hear too much about tools other than Spring. Is it because nothing comes closer? Is it performance? Is just simplicity of development? Is it because almost every java developer knows it?

After using it for quite a few years, I have come to ask myself these questions and wonder what else was out there.

Yet I noticed some companies with really high performance and time-to-market requirements were trying to move away from it. I worked for one and heard about the other one… and began to think why companies of such caliber would do it.

The quest

So, to answer my questions, I have decided to set myself on a quest to research different libraries for the most common Spring features and write about them. Not only will I focus on how to use them, but also on the different issues I had to deal with when trying to make them work the way I wanted them to.

To achieve this, I have written a series of small projects that demonstrate each tool and, lastly, an integrated project the brings together all of them.

I hope you find this article useful to either give you a better understanding of these tools if you are using them at work or just learning; or to expand you vision of the java ecosystem and inspire you to make your own research on these or other libraries.

What will be covered in the series?

It is commonly known that by using Spring there are tons of modules you could add to you application. It is only a matter of browsing through the available dependencies of Spring Initializr.

However, there are tools to cover many Spring features without the need of using it. Still, given it is the most widely used framework in the java community nowadays, many tools come bundled with spring itself or there are pluggable modules for them. Some examples are Jooq, H2 or MongoDB.

The source code of all of these demos can be found on my github.

Dependency injection

This topic is probably the very first reason of why Spring was born in the first place. You should be familiar with concepts such as encapsulation, information hiding, design patterns or SOLID principles, especially Dependency Inversion and Single Responsibility. Those concepts will allow you to have a proper class design in your system. For sure, once does not have to be uncle bob to write properly designed applications.

Yes, you could write your own dependency injection mechanism if you wanted to… I mean, it would be as simple as:

class MyDependencyInjector {   private UserDao userDao;
private UserService userService;

public MyDependencyInjector() {
this.userDao = new MongoDBUserDao();
this.userService = new UserService(this.userDao);
}
// getters
}

That could work but it will not scale for a real world application in which you may have different environments where it could run, configurations, tests with mocks, etc. You can go ahead and write it, but you would be re-inventing the wheel. There are libraries that solve this matter.

Dependency injection with Google Guice

You can find the demo here.

Google Guice is a library that handles dependency injection without any sort of package scanning.

The dependencies are resolved at runtime, which means that the more complex your dependency graph is, the longer it will take your app to boot.

It is based on Modules that define the bindings you want, either bind an interface to an implementation or simply state that some class is to be used as a dependency:

To create a module you need to extend from AbstractModule. In the example above, I have defined 3 modules: Common, Dev and Prod. The common dependencies are to be loaded regardless of the environment while one of the other 2 modules will be loaded depending on the environment.

There are more options to configure your dependencies, but here you can see the 3 basic ones:

  • Simply have a bind without the to. That tells Guice to simply create an instance of UserService as it does not have an interface.
  • Have a bind with a to. This tells Guice to bind an interface to a specific implementation. In this case I wanted redis for production and an in-memory solution for development.
  • Sometimes, especially with third-party code, you need to construct the object yourself and then configure it, like you would do for a datasource or redis in my example. For that you use the @Provides annotation. As the word says, it tells Guice that whenever a Jedis instance is needed, it is that method that provides it.

The scope tells Guice how to treat the dependency, ie: if it is singleton, reuse the instance you have, or create a new one every time.

The other side of this is to inject the dependencies. For that, Guice relies on 2 things: an @Inject annotation and an injector. You create the injector with the list of modules you want and then you instruct it to inject the dependencies in the target the class (the one that has the @Inject on its attributes). The @Inject could either be javax.inject.Inject or com.google.inject.Inject, both work.

The following code illustrates how you load the modules using the Injector. I have created a DependencyGraph class to encapsulate the responsibility.

The injector groups modules together. Then it is up to you how well you split the dependencies among them.

That is it for the most part. Of course you can write something different, like having multiple injectors, a different approach to modules… it is up to you! you have the tools to play around.

And one last bit is about testing. For JUnit5 you will need a non-official extension name.falgout.jeffrey.testing.junit5, although the creator works for google.

Dependency injection with Google Dagger

You can find the demo here.

Dagger is yet another library by Google. Unlike Guice which is 100% code based, Dagger is annotation based + generated code. Ie: you annotate your code and then you need to build the project. Once that is done, you will have generated a static dependencies graph. All resolved at compile time which means that the dependency graph is all set before you even boot your app thus making it boot faster.

To be honest, I had a really hard time trying to make it work. The concepts are a bit hard to grasp at first and the documentation it is not very good in in opinion. Once you understand how it works, it is easy to build on top of it.

For the setup, it requires specific configuration in your pom file: the library itself plus the compiler configuration for annotationProcessorPath and adding the generated code as part of your source. See the following configuration:

Like Guice, it is based on modules. The modules are grouped in components. The components create the dependencies. But for this, you need to build the project to create the component builder and only then you can use it.

The dependencies need to define an @Inject constructor, even if the only constructor is the default one because Dagger does not support injecting into private fields. In this case, it the javax.inject.Inject. And you will need to define a scope which you also do in the class by annotation it with @Singleton from javax.inject.Singleton.

Another interesting feature Dagger has but that I have not included here are the Lazy dependencies. This is, as its name states, a dependency that is loaded when it is used for the first time. An example would look like this:

There is one quote I read somewhere that really helped my head click: Use @Module to tell Dagger the things it cannot figure out itself, ie: datasource configuration, implementation of interfaces. What this means is that you do not need to define every dependency, just those that need clarification: Those that need configuration or those that have an interface with multiple implementations and you need to pick one.

Have a look at the following code:

As you can see, the only dependencies that need clarification are the Jedis because it should be configured (I used the default configuration for simplicity) and the UserKeyValueStore which has 2 different implementations. But for example, the UserService is not there and this is how it looks:

Nothing related to Dagger here as you can see.

Now, the other leg: Creating the dependencies. As stated, you will need to build your application whenever you change a dependency graph: add or remove a dependency, create a new module or a new component.

The component is the one in charge of loading the dependency graph. For that purpose, I have built a DependencyGraph class to encapsulate the responsibility:

Both DaggerDependencyGraph_DevComponent and DaggerDependencyGrapdh_ProdComponent were generated during the project build.

The component groups modules together. Then it is up to how well you divide the dependencies between them. Also, the component exposes which dependencies it can provide and, thus, it can be used to manage dependencies visibility.

When it comes to testing, you do not need anything special. You simply create your test component with the modules you want. But on the other hand, I could not find an elegant way to use mocks.

A simple test using could look like the following, of course you do not need Dagger fo unit testing.

Jumping ahead a little bit. When I was writing the full-demo I ran into a situation where I had to either mock an API call or use Wiremock. I went for the mock approach given that it is a common practice when testing with Spring. For this, I found no elegant solution rather than creating a new module that returns some dependency as a mock or a spy.

Configuration properties

Being able to externalise configuration such as database connection data, API urls, ports, etc is a very basic feature every software needs to have to avoid having to recompile everything whenever some value changes and to have a single place where that data can be found.

Of course it not always that simple and there are a few things to keep in mind when you want to manage the properties yourself, things Spring already solved:

  • The configuration will depend for sure on the environment where the application is running. You will have the test db which is obviously different from the production db. So you need to have different configuration files per environment:
application-dev.propertiesdb.host=10.15.51.24application-prod.propertiesdb.host=20.25.71.240
  • One thing that we always want in software development is to avoid repeating things. When something needs to change you do not want become Indiana Jones finding the lost ark… you want change it in one place. Some configuration values need to be the same across all configuration files. So this means that if there are 3 different configuration files you will need to change the value in all 3 of them? You do not want that. You need to have a file to store configuration that is valid for all environments and then put the environment specific values in the right configuration file.
application-common.properties

app.port=8080
application-dev.propertiesdb.host=10.15.51.24application-prod.propertiesdb.host=20.25.71.240
  • For now I have focused on single value properties that can simply be read into a variable. But what if you wanted to read a group of properties that belong to the same concern? For example, the DB connection requires a host, user, password and db name at least. You could read all of them into simple Strings/Numbers, etc. Things will get tricky if you want to read something as a List, Map or an Object. Objects will be even trickier if you have one in the common configuration with one or more properties overridden in the environment specific file. You will need to merge them. We will come back to this later.
application-common.properties

db.user=user
db.pass=pass
application-dev.propertiesdb.host=10.15.51.24application-prod.propertiesdb.host=20.25.71.240

Configuration properties with Apache Commons Configuration 2

You can find the demo here.

Apache Commons Configuration 2 is a popular library to be used when trying to have configuration files with Spring.

It offers many different configuration options:

  • Read one single configuration file.
  • Create a composite configuration made up from multiple files.
  • Upon configuration keys clash, it has different approaches to manage it: Union, Combine or override.
  • Supports properties, yaml and xml formats.

Once you setup your configuration, it offers typed methods to access the value, ie: getInt, getString, getList, etc. For the list, you can configure a ListDelimiterHandler to specify the delimiter character of the elements.

As with the dependency injection, it is up to you to define the usage: formats, file names, load one file per env or load a common file + the environment, the strategy to handle clashing keys, etc.

The following is an example of I configured my configuration 🤪

As for retrieving values, it is simple. However, if you want to mimic Spring and load a configuration class with the values of some properties you have to either do it manually or play with reflection. The is a method <T> T get(Class<T> cls, String key); which in theory should load the object but I could not make it work.

Also, you do not have the @Value("${myconfig.value}") feature that Spring has.

Database

In this case there is not too much to explain, it just a matter of accessing the data and managing transactions. I have already covered most of Jooq in my previous article. There is nothing really new here, except for the transaction management given there is no Spring and its @Transactional annotation. This is a replacement for spring-data-jdbc.

You can find the demo here.

For the most part, you need to connect to the DB manually, either using Jooq or some other tool like Hikari.

As for the transaction management, here is some official documentation. Given the way I have written the code, the option that look cleaner for me was to use the ThreadLocalTransactionProvider. Otherwise I would have had to propagate repository code into my service. I gave it a go anyway and I ended-up passing the Jooq configuration through my layers to be able to do this in my repository query: DSL.using(configuration). See this response from Jooq’s author.

Rest controller

Micro-services and REST APIs within micro-services are widely used nowadays. A REST API is an HTTP-based interface where the application receives requests to perform an operation. For this, spring-web comes bundled with annotations like @RestControllers, @GetMapping, @PostMapping, etc.

Rest controllers with Spark

You can find the demo here.

Sparkjava is a micro framework for creating web applications. It not only supports REST APIs, but also supports web-sockets and serving static content and templates with Velocity and Mustache for example.

Based on Jetty, I found this framework to be super light-weight and quick to setup, it just works out-of-the-box. All you need to do for a basic setup is define a port and some routes, that is it.

As any other REST API framework, it supports all the HTTP verbs, path params, query params and request body. A cool feature it has is the support for nested routes. One thing to notice is that you always have to return something, even if you want an empty response body. If you want to serialise something into JSON for example, you can either return a JSON string (not nice) or define a response transformer for every route.

Also, you can define interceptors for before and after the requests. And for the error management, you can define handlers for exceptions.

Rest controllers with Javalin

You can find the demo here.

Javalin is a close cousin of Sparkjava, it actually started as fork of it. It is also based on Jetty.

In terms of features, you can do the same things but with different syntax. Here is a comparison.

In Javalin, the routes do not have a return value and so the code looks nicer when you do not want to return something. However, if you want to return JSON, you need to wrap your application call in a method that will then serialise your response as such. To achieve this, you need to register a json transformer only once and then it used under the hood. It supports both GSON and Jackson.

The setup is just as easy:

Adding routes is also roughly the same, except there are no nested routes here:

The interceptors and error handling is also super similar:

Http Client

An http client is simply code to perform api calls to other services. In Spring, this is known as rest template. Even java introduced its own on JDK 11.

For the most part choosing an API client relies a bit on personal taste: It is about how easy it is to configure, how reusable, error handling, how clear the code is, if it can handle async or reactive requests.

Http Client with Retrofit

You can find the demo here.

Retrofit is a high-level http client based on OkHttp. It abstract the developer from almost everything that takes place on an http request.

With Retrofit you simply define an interface annotated with the request information (path, method, params, query, headers, etc) and then the library takes care of connectivity, serialisation/deserialisation, etc under the hood using OkHttp.

It can perform sync/async queries and it is quite popular around android developers.

Here is how you configure the interface:

To use it, you just have to build it first!

Then you are good to go. You can do both sync/async calls:

Http Client with OkHttp

You can find the demo here.

OkHttp is a low level implementation of an http client where the developer must do everything manually, Retrofit is built on top of this.

It can also perform sync/async queries.

This is how you would use it:

And finally putting it all together…

Lastly, reaching the end of the quest. As last step I wrote an all-in-one application that makes use of most of the tools explained here. I chose Dagger, Retrofit and Javalin combined with Apache commons config and Jooq.

The domain is similar to the one I used in my JOOQ demo. The only change is that I have added the concept of a User who is looked-up in https://jsonplaceholder.typicode.com/users/1 and stored in the DB.

When it came to decide how to do things, ie: choose library, approaches, design, etc, I went with what I thought was best for the use-case in particular and leaned towards some standards similar to what Spring does so it is easier to compare.

There is really nothing special to explain here that has not been covered already other than saying I really enjoyed building the application 😄.

You can find the full code here.

Conclusions

So, the quest has ended 😅. Here are some thoughts on the tools I have reviewed.

Dependency injection conclusions

Both Guice and Dagger offer quite a few options to create a dependency graph. I think both do a good job and they can for sure be a replacement for the Spring dependency injection mechanism.

Both of them have a learning curve, and I found that, for Dagger in particular, it was very steep. Yes, unlike Guice, Dagger has a website and tutorials. But honestly I could not find it super helpful and most of my answers came -obviously- from Stackoverflow. Once you grasp it, it is actually quite good.

Another “advantage” that I have found by using these tools is that they force you to really think how to structure your modules/dependencies to make them reusable and testable. It enforces learning in the end and that is super valuable for a developer.

With Dagger in particular, you can learn a lot by looking at the generated source code. You will notice that it is all pure java, no proxies or aspected involved. Also, when something does not work or compile, Dagger will print some rather useful errors. Then go have a look at that generated code and it is likely you will find the issue. For example, at some point everything was working but then I added logs and noticed that some lines were appearing many times… By looking the generated code, I realised that one class was not effectively a singleton, I had missed the annotation and did not notice. In the generated code, Dagger was creating an instance of my dependency using a new every time.

Configuration properties conclusions

This is probably the most limited replacement tool of all present in this article. Yes, it does the job and it is easy to setup and use. But I think there is just too much to do by hand. Do not get me wrong, it can be done but it seems to me that if you want to have a complex set of configurations and replicate all or most of the Spring features… that would be a project on its own.

If you keep it simple to a certain level, this is a good replacement. I have used this (version 1 actually) at professional level and it got the job done. The application was extremely critical for the business and had quite a few configurations, nothing super complex of course. We kept it simple.

Database conclusions

As I stated in my previous article, Jooq is amazing and so easy to use. Just like everything, you need to learn how to use it, but once you are there it is a bliss. It can perfectly replace spring-data-jdbc and JPA.

Rest controller conclusions

Both Sparkjava and Javalin are good, quick and easy replacement for Spring-web. You can do the same things just as easy. Maybe the code may not look so elegant with the fancy annotations and all but it can get the job done just as good.

Http Client conclusions

In this case, I think the biggest advantage you get with both Retrofit and OkHttp is the chance of execution asynchronous calls. Spring rest-template is synchronous and if you want something async you will need to use spring reactive’s org.springframework.web.reactive.function.client.WebClient.

Retrofit offers exactly the same as spring-feign does, but with the async capabilities. You also get around with something less abstracted like OkHttp or the java 11 http client.

Final words

After going through this quest, I can say that it is amazing how many things Spring solves for you and how easy it is to use. I agree though that it has too many levels of abstraction, proxies, aspects and it may impact performance and specially boot time. But there things are just plug-and-play even when it comes to Maven configuration. Spring boot apps come bundled with many maven plugins that work out of the box like the compiler and packaging.

The biggest issue with Spring is, when something fails, trying to figure out what failed. Most of the time it is something obvious, but when it is not… Remember that to make things easier for the developer, Spring does tons of hardcore stuff under the hood with reflection, proxies and aspects. Those things do not come for free and sometimes it feels like black magic.

On the other side, I reckon it was fun to research these tools. I forced me to learn and refresh many things that I had forgotten after so many years working with Spring. It forces you to choose your path of design, to define how hard or how easy and configurable you app needs to be… It gives you power in the end, the chance to control everything that is happening and that is how you learn.

As you could see, most of this tools are more than fine to replace Spring. My only asterisk would the properties which could get super complex if you have to write too much code just to read them.

I think that biggest arguments for companies to choose Spring over other tools are:

  • Setup and development speed: As stated, Spring solves a lot of things for you. You do not need to write complex code for configuration properties or learn how some dependency injection engine works, etc. Spring has modules for everything. Besides, given it is the tool choice of the vast majority, almost everyone knows the basics.
  • Documentation: Needless to say that the spring community is huge. Given it is the more widespread framework, you will find an answer to any issue a few click away.

My conclusion today is that for large scale/monolithic application, consisting of many modules and different configurations, I would still choose Spring purely because I have experienced big issues with configurations and dependencies. Specially with configuration, I found these topics to be quite complex even with this small toy project. The bigger the scope, the bigger the issues you could face and I think that dealing with dependency injection/configuration issues is not something else you want to have issues with. You need a strong foundation.

I would definitely use these technologies for a micro-service application with well defined scope. Maybe if I start using this at a professional level or maybe a real project of my own I would become fluent and finally discard Spring.

At the end of the day, it all comes down to your requirements. If you find yourself having to use too many spring features not covered by this article like JMS, Kafka, reactive, security, etc… basically if you go Spring Initializr and end up ticking a bunch of them, chances are you are better off with Spring. There is no point on reinventing the wheel.

It is not impossible, obviously, to write complex applications without Spring, it just depends on how much time you have to finish the project and what is the level of knowledge among the team members.

I hope you have enjoyed this article!! 😄

--

--

Ignacio Cicero

I’m a back-end software engineer working in finance. I write about Java and tech that I decide to research.