3. Data - Spring Boot vs Micronaut Framework - Building a Rest API

This guide compares how to add Spring Data or Micronaut Data to power a GET endpoint in both Micronaut and Spring Boot applications.

Authors: Sergio del Amo

Micronaut Version: 4.6.1

1. Sample Project

You can download a sample application with the code examples in this article.

2. Introduction

This guide compares how to power a GET endpoint with a persistence layer in a Micronaut Framework and Spring Boot applications.

The Spring Boot application uses Spring Data and H2. The Micronaut application uses Micronaut Data and H2.

This guide is the third tutorial of Building a Rest API - a series of tutorials comparing how to develop a REST API with Micronaut Framework and Spring Boot.

Micronaut Data is a database access toolkit that uses Ahead of Time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer.

2.1. Micronaut Data vs Spring Data

Micronaut Data improves on Spring Data in the following ways:

  • No runtime model - Spring Data maintains a runtime metamodel that uses reflection to model relationships between entities. This model consumes significant memory and memory requirements grow as your application size grows. The problem is worse when combined with Hibernate which maintains its own metamodel as you end up with duplicate meta-models.

  • No query translation - Spring Data uses regular expressions and pattern matching in combination with runtime generated proxies to translate a method definition on a Java interface into a query at runtime. No such runtime translation exists in Micronaut Data and this work is carried out by the Micronaut compiler at compilation time.

  • No Reflection or Runtime Proxies - Micronaut Data uses no reflection or runtime proxies, resulting in better performance, smaller stack traces and reduced memory consumption due to a complete lack of reflection caches (Note that the backing implementation, for example Hibernate, may use reflection).

  • Type Safety - Micronaut Data will actively check at compile time that a repository method can be implemented and fail compilation if it cannot.

3. Dependencies

3.1. Spring Boot Data and H2

To use Spring Data JDBC and H2 add the following dependencies:

build.gradle
implementation("org.springframework.data:spring-data-jdbc")
runtimeOnly("com.h2database:h2")

3.2. Micronaut Data and H2

To use Micronaut Data JDBC and H2 add the following dependencies:

build.gradle
annotationProcessor("io.micronaut.data:micronaut-data-processor")
implementation("io.micronaut.data:micronaut-data-jdbc")
runtimeOnly("com.h2database:h2")

Please note the addition of the micronaut-data-processor in the annotation processor classpath. Micronaut Data does a lot of work at compilation, which leads to applications with better performance and a reduced memory consumption.

4. Entities

We modified the SaasSubscription Java Record. We use it as a persistence entity.

4.1. Spring Boot

For Spring Data, we added the @Id annotation.

springboot/src/main/java/example/micronaut/SaasSubscription.java
package example.micronaut;

import org.springframework.data.annotation.Id;

record SaasSubscription(@Id Long id, String name, Integer cents) {
}

4.2. Micronaut

For Micronaut Framework, the entity is annotated with @MappedEntity.

micronautframework/src/main/java/example/micronaut/SaasSubscription.java
package example.micronaut;

import io.micronaut.serde.annotation.Serdeable;
import io.micronaut.data.annotation.Id;
import io.micronaut.data.annotation.MappedEntity;

@Serdeable (1)
@MappedEntity (2)
record SaasSubscription(@Id Long id, (3)
                        String name,
                        Integer cents) {
}
1 Declare the @Serdeable annotation at the type level in your source code to allow the type to be serialized or deserialized.
2 Annotate the class with @MappedEntity to map the class to the table defined in the schema.
3 Specifies the ID of an entity

5. SQL

Spring Boot loads SQL from the standard root classpath locations: schema.sql and data.sql. Micronaut Framework recommends you manage your database schema with Liquibase or Flyway. However, to keep applications as close as possible, we will create the database schema with an SQL file in the Micronaut application. Micronaut Test supports loading SQL before tests seamlessly. To keep both apps as close as possible,

We will use two SQL files in the tests:

  • src/main/resources/schema.sql.

  • src/main/resources/data.sql.

5.1. SQL Schema

micronautframework/src/test/resources/schema.sql
CREATE TABLE IF NOT EXISTS saas_subscription
(
    id     BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name  VARCHAR(255) NOT NULL,
    cents NUMBER NOT NULL DEFAULT 0
);

5.2. Sample Data

micronautframework/src/test/resources/data.sql
INSERT INTO saas_subscription(id, name, cents) VALUES (99, 'Advanced', 2900);

6. Repository

Both Micronaut Data and Spring Data enable developers to use the repository pattern. You write an interface and the framework provides the implementation.

6.1. Spring Boot Repository

springboot/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import org.springframework.data.repository.CrudRepository;

interface SaasSubscriptionRepository extends CrudRepository<SaasSubscription, Long> {
}

6.2. Micronaut Repository

Because Micronaut Data works at build-time, you have to specify the dialect.

micronautframework/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.CrudRepository;

@JdbcRepository(dialect = Dialect.H2) (1)
interface SaasSubscriptionRepository extends CrudRepository<SaasSubscription, Long> { (2)
}
1 @JdbcRepository with a specific dialect.
2 By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations.

7. Controller

We modified the controller we wrote in the previous tutorial to inject the repository. We use constructor injection in both applications.

7.1. Spring Boot Controller

springboot/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController (1)
@RequestMapping("/subscriptions") (2)
class SaasSubscriptionController {

    private final SaasSubscriptionRepository repository;

    private SaasSubscriptionController(SaasSubscriptionRepository repository) {
        this.repository = repository;
    }

    @GetMapping("/{id}") (3)
    private ResponseEntity<SaasSubscription> findById(@PathVariable Long id) { (4)
        return repository.findById(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}
1 Annotate a class @RestController to identify the class as a Component capable of handling HTTP requests.
2 @RequestMapping identifies the request paths that invoke this Controller.
3 The @GetMapping annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
4 Add the @PathVariable annotation to the handler method argument to make controller aware.

7.2. Micronaut Controller

micronautframework/src/main/java/example/micronaut/SaasSubscriptionController.java
package example.micronaut;

import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;

@Controller("/subscriptions") (1)
class SaasSubscriptionController {

    private final SaasSubscriptionRepository repository;

    SaasSubscriptionController(SaasSubscriptionRepository repository) {
        this.repository = repository;
    }

    @Get("/{id}") (2)
    HttpResponse<SaasSubscription> findById(@PathVariable Long id) { (3)
        return repository.findById(id)
                .map(HttpResponse::ok)
                .orElseGet(HttpResponse::notFound); (4)
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 The @Get annotation maps the findById method to an HTTP GET request on /subscriptions/{id}.
3 You can define path variables with a RFC-6570 URI template in the HTTP Method annotation value. The method argument can optionally be annotated with @PathVariable.
4 You can use the HTTPResponse fluid API to control the response’s status code, its body and headers.

8. Tests

In this tutorial, we use AssertJ in the tests. Moreover, we use Jayway JsonPath - a Java DSL for reading JSON documents.

8.1. Micronaut Test

The Spring boot tests are identical to those in the previous tutorial. In the Micronaut Test, we need to annotate the test with @Sql to load the SQL files before the tests.

micronautframework/src/test/java/example/micronaut/SaasSubscriptionControllerGetTest.java
package example.micronaut;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowableOfType;
import io.micronaut.test.annotation.Sql;

@Sql(value = {"classpath:schema.sql", "classpath:data.sql"}) (1)
@MicronautTest (2)
class SaasSubscriptionControllerGetTest {

    @Inject
    @Client("/")
    HttpClient httpClient; (3)

    @Test
    void shouldReturnASaasSubscriptionWhenDataIsSaved() {
        BlockingHttpClient client = httpClient.toBlocking();
        HttpResponse<String> response = client.exchange("/subscriptions/99", String.class);
        assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());

        DocumentContext documentContext = JsonPath.parse(response.body());
        Number id = documentContext.read("$.id");
        assertThat(id).isNotNull();
        assertThat(id).isEqualTo(99);

        String name = documentContext.read("$.name");
        assertThat(name).isNotNull();
        assertThat(name).isEqualTo("Advanced");

        Integer cents = documentContext.read("$.cents");
        assertThat(cents).isEqualTo(2900);
    }

    @Test
    void shouldNotReturnASaasSubscriptionWithAnUnknownId() {
        BlockingHttpClient client = httpClient.toBlocking();
        HttpClientResponseException thrown = catchThrowableOfType(() -> (4)
                client.exchange("/subscriptions/1000", String.class), HttpClientResponseException.class);
        assertThat(thrown.getStatus().getCode()).isEqualTo(HttpStatus.NOT_FOUND.getCode());
        assertThat(thrown.getResponse().getBody()).isEmpty();
    }
}
1 @Sql allows you to load SQL before tests
2 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
3 Inject the HttpClient bean and point it to the embedded server.
4 When the HTTP Client receives a response with an HTTP Status Code >= 400, it throws an HttpClientResponseException. You can obtain the response status and body from the exception.

9. Conclusion

Adding a persistence layer is easy in both frameworks, and the API is almost identical. However, Micronaut Data’s compile-time/reflection-free approach results in better performance, smaller stack traces, and reduced memory consumption.

10. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).