Just start typing...
Technologies

Spring vs. Micronaut: we created two applications to find out which framework is better

Published September 14, 2022

Spring Framework has a lot of advantages when it is used for building Enterprise applications, but it may also have some disadvantages. For example, it can appear somewhat complicated, and it also consumes a lot of CPU resources. In this article we compare Spring with Micronaut and share the process and the results. 

Let's compare the two frameworks now!

Intro

Graeme Rocher, the author of Grails Framework, decided to create a new tool which would be more lightweight than Spring Framework. As a result of his experience in using Spring, Spring Boot and Grails, he eventually developed Micronaut Framework.

As specified on Micronaut's official website, it has a lot of advantages, e.g.:

  • Compatibility with Java, Groovy, and other JVM programming languages.
  • Fast database access configuration.
  • Simple unit tests creating.

Let’s look at the main differences between these frameworks more closely, and let’s decide which tool is better.

The main differences

These frameworks are quite similar in development. They both have DI support, configuring, creating APIs, and so on. For example, this is what creating a controller in Micronaut will look like:

  
@Controller(“/api/department”)
public class DepartmentController {
  @Inject
  private DepartmentService departmentService;

  @Get
  public List findAll() {
    return departmentService.findAll();
  }
}
  
  
Controller

The Spring Framework annotation @Controller indicates that endpoints should return View (if they are not annotated with @ResponseBody). But in Micronaut, this annotation will return a mapped (in JSON) object. This approach looks more reasonable because in modern development, web pages are rarely rendered on the backend side.
The @Inject annotation is like @Autowired from Spring and allows inject dependencies through a class field. It is better to inject dependencies through a constructor or setter, which Micronaut can also do without any problems.

Beans

Micronaut has beans scope like Spring. However, their declarations slightly differ. To declare the scope of a bean in Spring, we need to write the @Scope annotation and indicate the scope in this annotation. But Micronaut has separate annotations for each scope – Singleton, Prototype, Context, ThreadLocal, Infrastructure, and Refreshable.

Application launch speed. Memory usage

As we mentioned before, the main aim of the creation of Micronaut was to make it lightweight, so that it could launch quickly. Let’s check and compare this. For that, we’ve created two simple applications with Web support via Spring and Micronaut.
Creating Micronaut project in IntelliJ IDEA is simple:

Project creating in IntelliJ IDEA

When creating a project this way, it will be specified in the dependencies that the Netty web server will be used in our application. Let’s change the web server to Tomcat:

  

  io.micronaut.servlet
  micronaut-http-server-tomcat

  
  

For clarity purposes, let’s compare launch speed in Spring Boot, Spring Native, and Micronaut both in Lazy and Eager modes.

Disclaimer: Spring Native, at the time of writing the article, is experimental. This technology should solve Spring’s problem with the working speed. This is accomplished by using GraalVM, which, unlike JVM, launches faster and consumes less memory.

Let’s compare the Lazy and Eager modes in Micronaut. Micronaut starts in the Lazy mode by default, and all beans are initialized only at the moment when the application is going to use them. The Eager mode initializes all beans in the bootstrap. To launch an application in the Eager mode, let’s add this code in a base class:

  
public class Application {
    public static void main(String[] args) {
        Micronaut.build(args)
            .eagerInitSingletons(true)
            .mainClass(Application.class)
            .start();
    }
}
  
  

Let’s create applications, and check out their launch times. Our applications will be simple, with one controller and one endpoint without any logic, and then we will add these applications to Docker. After that, we will run these containers several times and check the launch time.

Project's launch speed

We can see that Spring Native is faster than other frameworks. This is achieved by Spring Native having no redundant dependencies.

Spring Boot is the slowest framework in this graph.

As we can see, Micronaut in the Lazy mode launches slower than Spring Native, but faster than any other framework. Also, we can see that Micronaut Lazy has a more stable graph than Micronaut in the Eager mode and Spring Boot. However, using Micronaut Eager slows down startup time.

Ok, let’s check the memory usage of these frameworks. For that, we can use Docker, which can show us some statistics (CPU usage, memory usage, etc.).

Project's memory usage

As we can see in the graph, memory usage is correlated with startup time.

Let’s create a more complex application and check launch time and memory consumption.

Creating an application

Let’s imagine that we have a task of creating an application that can accept some documents, and that application should parse them, and write this data to a database.

For that, we created two services. The first will accept Multipart files through an endpoint, validate them, and send these files to the message broker. The second will read the documents from the broker, parse them, and write the information to the database.

Producer’s sequence diagram

Consumer’s sequence diagram

First we will create this application via Micronaut.

In order to implement this program, we want to create a producer and a consumer. As a message broker, we are going to use Apache Kafka. To add Kafka support, we need to add the following dependency:

  

  io.micronaut.kafka
  micronaut-kafka

  
  

Then, we are going to configure a connection to the broker in services. In Micronaut we can do it easily with the following configuration:

  
kafka:
  bootstrap:
    servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:19092}
  
  

Producer code snippet:

  
@KafkaClient
public interface DocumentProducer {

    @Topic("document")
    void sendDocument(DocumentDto documentDto);

}
  
  

Consumer code snippet:

  
@KafkaListener(offsetReset = OffsetReset.EARLIEST)
public class DocumentListener {

    private final DocumentService documentService;

    public DocumentListener(DocumentService documentService) {
        this.documentService = documentService;
    }

    @Topic("document")
    public void listen(DocumentDto documentDto) {
        documentService.proceedDocument(documentDto);
   }

}
  
  

As we see, configuring asynchronous messaging is easy. Next, we are going to configure database support.

Unlike Micronaut, Spring Boot has native dependencies that have useful built-in dependencies for all cases. In Micronaut, we are going to add these dependencies manually:

  

	io.micronaut.configuration
	micronaut-jdbc-hikari
	2.2.6


	io.micronaut.data
	micronaut-data-hibernate-jpa


	javax.annotation
	javax.annotation-api
	1.3.2


	javax.persistence
	javax.persistence-api
	2.2


	org.postgresql
	postgresql
	42.4.0

  
  

Database configurations:

  
datasources:
  default:
    url: jdbc:postgresql://${DATABASE_URL}/document-db
    driverClassName: org.postgresql.Driver
    username: admin
    password: admin

jpa:
  default:
    packages-to-scan:
      - 'com.waveaccess.consumer.model'
  
  

It should be noted that beans management in Micronaut is less flexible than in Spring. For example, in large projects, migrations are done via some libraries (Flyway, Liquibase, etc.). However, in some cases, we need to take some actions before starting migrations. In Spring, we can indicate dependencies between beans and manage the order of initializing. Micronaut cannot do that.

For example, we want to start Liquibase migrations before Hibernate to check that Liquibase creates databases correctly. We can add the following construction in Spring:

  
@Bean
@DependsOn(“entityManagerFactory”)
public SpringLiquibase springLiquibase() {
	. . .
}

@Bean
public  LocalContainerEntityManagerFactoryBean entityManagerFactory() {
	. . .
}
  
  

As for Micronaut, we can solve this problem with any other approach (e.g., first running Liquibase and then an application with Hibernate). 

After Micronaut, let’s create a similar application via Spring and check the launch time for producer and consumer services:

Producer's startup speed

Green dots – Spring, blue ones – Micronaut.

Consumer's startup speed

So, as we can see, Micronaut is much better than Spring. It’s achieved by using Lazy mode.

Let’s load the applications, and check the statistics. To do that, we create 100 mock users who will send documents via JMeter. Also, we will run the applications like a plain JAR files.

System overview:

CPU: 2600MHz, 6/12

RAM: 8/4, 3200MHz

First, we will check the Spring application. We use VisualVM application for monitoring, which will show us CPU and memory consumption in the form of graphs.

Spring Boot. Consumer application. Consuming without loading

Spring Boot. Producer application. Consuming without loading

Let’s DDOS-attack them, and we’ll see the following result:

Spring Boot. Consumer application. Consuming at loading

Spring Boot. Producer application. Consuming at loading

As we can see, memory consumption is raised slightly, but our consumer starts using a lot of CPU resources.

Now it is Micronaut’s turn!

Micronaut. Consumer application. Consuming without loading

 

Micronaut. Producer application. Consuming without loading

The same application after DDOS-ing it:

Micronaut. Consumer application. Consuming at loading

Micronaut. Producer application. Consuming at loading

As we can see, the graphs for the frameworks are not the same. The Producer application in Micronaut loads the system havier than in Spring Boot. But if you look at memory consumption, the frameworks perform almost identically. 

Application testing

Let’s try creating tests for our application. Maybe we will see some differences between the frameworks here.

As a first step, we want to create unit tests to check file parsing. In Micronaut, we can add dependencies on Micronaut Test and choose a testing library among these:

  • JUnit
  • Spock
  • Kotest

We choose Junit, and add the following dependency:

  

  io.micronaut.test
  micronaut-test-junit5

  
  

Here is the simple test:

  
class ReportTableParserTest {

    @Inject
    ReportTableParser parser;

    @Test
    public void shouldParseFile() throws IOException {

        // GIVEN
        DocumentDto document = DocumentHelper.createDocument(DocumentType.REPORT);

        // WHEN
        ReportDto result = parser.parse(document);

        // THEN
        assertNotNull(result);
        assertAll(
                () -> assertEquals(result.getAuthor(), "Me"),
                () -> assertEquals(result.getDescription(), "TEST"),
                () -> assertEquals(result.getPriority(), 4)
        );
    }
}
  
  

So, there’s not any exotic code. The only difference is in Micronaut annotations.

Annotation @MicronautTest indicates that the class should run all methods with @Test annotations according to the chosen testing framework. These annotations are located in packets according to a framework:

  • io.micronaut.test.extensions.spock.annotation.MicronautTest
  • io.micronaut.test.extensions.junit5.annotation.MicronautTest
  • io.micronaut.test.extensions.kotest.annotation.MicronautTest

Here’s the similar test in Spring:

  
@SpringBootTest(classes = ReportTableParser.class)
class ReportTableParserTest {

    @Autowired
    ReportTableParser parser;

    @Test
    public void shouldParseFile() throws IOException {

        // GIVEN
        DocumentDto document = DocumentHelper.createDocument(DocumentType.REPORT);

        // WHEN
        ReportDto result = parser.parse(document);

        // THEN
        assertNotNull(result);
        assertAll(
                () -> assertEquals(result.getAuthor(), "Me"),
                () -> assertEquals(result.getDescription(), "TEST"),
                () -> assertEquals(result.getPriority(), 4)
        );
    }
}
  
  

Also, Micronaut has a different way of mocking objects:

  
@MicronautTest
class UserServiceTest {

    @Inject
    UserService userService;

   @Inject
    UserRepository userRepository;

    @Test
    public void shouldFindUser() {

        // GIVEN
        Long userId = 1L;
        stubUserRepository();

        // WHEN
        User user = userService.findById(userId);

        // THEN
        assertNotNull(user);
        assertEquals(userId, user.getId());
    }

    @MockBean(UserRepository.class)
    UserRepository userRepository() {
        return mock(UserRepository.class);
    }

    private void stubUserRepository() {
        when(userRepository.findById(1L))
                .thenReturn(UserHelper.createUser());
    }
}
  
  

The method that is annotated with @MockBean in this code creates mocking objects. However, there is a sort of "problem" with this method. To create an object, we need to add @Inecjt to the class field and mark it with the annotation otherwise nothing will be created.

Let’s create an integration test:

  
@MicronautTest
class DocumentServiceIT {

    @Inject
    DocumentService documentService;

    @Inject
    ReportRepository reportRepository;

    @BeforeEach
    public void init() {
        List reports = ReportHelper.createReportsList(10);
        reportRepository.saveAll(reports);
    }

    @Test
    public void shouldFindDocuments() {

        // WHEN
        List result = documentService.findAll();

        // THEN
        assertNotNull(result);
        assertFalse(result.isEmpty());
        assertEquals(result.size(), 10);
    }
}
  
  

As we can see, there is no big difference between Spring Boot and Micronaut.

Please find the source code by this link.

Conclusion

We compared Spring and Micronaut frameworks by creating applications on them with functionality of messaging, parsing, and working with databases.

As our research shows, Micronaut is lighter than Spring, which gives an advantage in startup speed and memory consumption. However, that doesn’t mean that all Spring applications are banned now, or that you should rewrite your code on Micronaut.

If you have a choice between these frameworks, then feel free to look at the next table and decide which are your priorities:

  Micronaut Spring
Startup speed FASTER SLOWER
Memory usage MORE LESS
Documentation availability LESS MORE
Community support LESS MORE

The first two criteria are quite obvious, while the other ones may need to be explained. Micronaut is a relatively new framework (released in 2018), and it is still gaining its popularity. Developers haven’t had enough time to explore it, unlike Spring. There is a shortage of books about Micronaut (again, unlike Spring).

It’s easy to create simple projects on Micronaut, but it may be more tricky to build more complex applications. You may face a situation of having a problem that is impossible to google, because no one has faced it before.

Also, as we can see, both frameworks have no differences in testing. 

We are sure that Micronaut Framework has potential, but it also needs more time to bloom. After all, even Spring needed some time before developers started switching to it from J2EE. 

Related Services

Automated Functional Testing
Automated Load Testing
Application Development

How we process your personal data

When you submit the completed form, your personal data will be processed by WaveAccess USA. Due to our international presence, your data may be transferred and processed outside the country where you reside or are located. You have the right to withdraw your consent at any time.
Please read our Privacy Policy for more information.