Warning

This article was automatically translated by OpenAI (gpt-4.1).It may be edited eventually, but please be aware that it may contain incorrect information at this time.

FerretDB is an open-source database compatible with MongoDB, using DocumentDB extensions and PostgreSQL as its backend.
While MongoDB has switched to the SSPL (Server Side Public License), FerretDB is licensed under Apache 2.0 (the DocumentDB extension is under the MIT license).

In this post, I'll jot down how to access FerretDB using Spring Boot + Spring Data MongoDB.
From the application's perspective, it's the same as MongoDB; only the configuration for Testcontainers changes.

Creating a Project Template

Use Spring Initializr to create a Spring Boot project template.

curl -s https://start.spring.io/starter.tgz \
       -d artifactId=demo-ferretdb \
       -d name=demo-ferretdb \
       -d baseDir=demo-ferretdb  \
       -d packageName=com.example \
       -d dependencies=web,data-mongodb,actuator,configuration-processor,prometheus,native,testcontainers \
       -d type=maven-project \
       -d applicationName=DemoFerretApplication | tar -xzvf -
cd demo-ferretdb 

Creating a Sample App

Let's create a very simple application to save and retrieve messages.

cat <<EOF > src/main/java/com/example/Message.java
package com.example;

public record Message(String id, String text) {
}
EOF
cat <<EOF > src/main/java/com/example/HelloController.java
package com.example;

import java.util.List;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    private final MongoTemplate mongoTemplate;

    public HelloController(MongoTemplate mongoTemplate) {
        this.mongoTemplate = mongoTemplate;
    }

    @PostMapping(path = "/messages")
    public Message postMessage(@RequestBody String text) {
        return mongoTemplate.save(new Message(null, text));
    }

    @GetMapping(path = "/messages")
    public List<Message> getMessages() {
        return mongoTemplate.findAll(Message.class);
    }

}
EOF

Since Testcontainers was added via Spring Initializr, the following file is included in the project.

package com.example;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

    @Bean
    @ServiceConnection
    MongoDBContainer mongoDbContainer() {
        return new MongoDBContainer(DockerImageName.parse("mongo:latest"));
    }

}

Let's start the app using Testcontainers. Either run src/test/java/com/example/TestDemoFerretApplication.java or execute the following command:

./mvnw spring-boot:test-run

Once the application and MongoDB are running, try POSTing & GETting messages as follows.

$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!"
{"id":"6879a6f3ba99e4ec5c9419fd","text":"Hello MongoDB!"}

$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello FerretDB\!"
{"id":"6879a6f8ba99e4ec5c9419fe","text":"Hello FerretDB!"}

$ curl -s http://localhost:8080/messages | jq .
[
  {
    "id": "6879a6f3ba99e4ec5c9419fd",
    "text": "Hello MongoDB!"
  },
  {
    "id": "6879a6f8ba99e4ec5c9419fe",
    "text": "Hello FerretDB!"
  }
]

The test code corresponding to this operation check is as follows.

package com.example;

import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClient;

import static org.assertj.core.api.Assertions.assertThat;

@Import(TestcontainersConfiguration.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class DemoFerretApplicationTests {

    RestClient restClient;

    @BeforeEach
    void setUp(@LocalServerPort int port, @Autowired RestClient.Builder restClientBuilder) {
        this.restClient = restClientBuilder.defaultStatusHandler(statusCode -> true, (req, res) -> {
            /* NO-OP */}).baseUrl("http://localhost:" + port).build();
    }

    @Test
    void contextLoads() {
        {
            ResponseEntity<Message> res = this.restClient.post()
                .uri("/messages")
                .contentType(MediaType.TEXT_PLAIN)
                .body("Hello MongoDB!")
                .retrieve()
                .toEntity(Message.class);
            assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
            Message message = res.getBody();
            assertThat(message).isNotNull();
            assertThat(message.text()).isEqualTo("Hello MongoDB!");
            assertThat(message.id()).isNotNull();
        }
        {
            ResponseEntity<Message> res = this.restClient.post()
                .uri("/messages")
                .contentType(MediaType.TEXT_PLAIN)
                .body("Hello FerretDB!")
                .retrieve()
                .toEntity(Message.class);
            assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
            Message message = res.getBody();
            assertThat(message).isNotNull();
            assertThat(message.text()).isEqualTo("Hello FerretDB!");
            assertThat(message.id()).isNotNull();
        }
        {
            ResponseEntity<List<Message>> res = this.restClient.get()
                .uri("/messages")
                .retrieve()
                .toEntity(new ParameterizedTypeReference<>() {
                });
            assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK);
            List<Message> messages = res.getBody();
            assertThat(messages).isNotNull();
            assertThat(messages).hasSize(2);
            assertThat(messages).map(Message::id).allSatisfy(id -> assertThat(id).isNotNull());
            assertThat(messages).map(Message::text).containsExactly("Hello MongoDB!", "Hello FerretDB!");
        }
    }

}

MongoDB will also start via Testcontainers during testing. You can run the tests with the following command:

./mvnw clean test

Switching to FerretDB

Let's switch from MongoDB to FerretDB. You don't need to change the application code; only the Testcontainers configuration needs to be updated.
Since the FerretDB container image is not compatible with MongoDBContainer, use GenericContainer instead.
Therefore, don't use ServiceConnection, but use DynamicPropertyRegistrar to dynamically register MongoDB connection info. Since FerretDB has authentication enabled by default, set spring.data.mongodb.uri including the username and password.

Change TestcontainersConfiguration as follows:

cat <<EOF > src/test/java/com/example/TestcontainersConfiguration.java
package com.example;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistrar;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy;
import org.testcontainers.utility.DockerImageName;

@TestConfiguration(proxyBeanMethods = false)
class TestcontainersConfiguration {

    @Bean
    GenericContainer<?> ferretDbContainer() {
        return new GenericContainer<>(DockerImageName.parse("ghcr.io/ferretdb/ferretdb-eval:2"))
            .withExposedPorts(27017, 5432)
            .withEnv("POSTGRES_USER", "user")
            .withEnv("POSTGRES_PASSWORD", "password")
            .withEnv("FERRETDB_TELEMETRY", "false")
            .waitingFor(new HostPortWaitStrategy().forPorts(27017, 5432));
    }

    @Bean
    DynamicPropertyRegistrar dynamicPropertyRegistrar(GenericContainer<?> ferretDbContainer) {
        return registry -> registry.add("spring.data.mongodb.uri", () -> "mongodb://user:password@%s:%d/test"
            .formatted(ferretDbContainer.getHost(), ferretDbContainer.getMappedPort(27017)));
    }

}
EOF

The same tests should pass after changing TestcontainersConfiguration:

./mvnw clean test

Either rerun src/test/java/com/example/TestDemoFerretApplication.java or execute the following command again:

./mvnw spring-boot:test-run

As before, try POSTing & GETting messages.

$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello MongoDB\!"
{"id":"6879af654c503243968ecba0","text":"Hello MongoDB!"}

$ curl http://localhost:8080/messages -H content-type:text/plain -d "Hello FerretDB\!"
{"id":"6879af6a4c503243968ecba1","text":"Hello FerretDB!"}

$ curl -s http://localhost:8080/messages | jq .
[
  {
    "id": "6879af654c503243968ecba0",
    "text": "Hello MongoDB!"
  },
  {
    "id": "6879af6a4c503243968ecba1",
    "text": "Hello FerretDB!"
  }
]

You can confirm that switching from MongoDB to FerretDB works without any issues.

Running Standalone

Let's try running the application standalone, without Testcontainers.

Start FerretDB with the following docker run command:

docker run --rm --name ferretdb -p 27017:27017 -e POSTGRES_USER=user -e POSTGRES_PASSWORD=password -e FERRETDB_TELEMETRY=false ghcr.io/ferretdb/ferretdb-eval:2

Create an executable jar file with the following command:

./mvnw clean package

Specify spring.data.mongodb.uri at runtime to connect to FerretDB.

java -jar target/demo-ferretdb-0.0.1-SNAPSHOT.jar --spring.data.mongodb.uri=mongodb://user:password@localhost:27017/test

You should be able to POST & GET messages as before.

The source code used for this operation check is here.


This post introduced how to use FerretDB with Spring Boot + Spring Data MongoDB + Testcontainers.
By using FerretDB as an alternative to SSPL-licensed MongoDB, you can avoid licensing issues while still using MongoDB-compatible features.

For detailed compatibility, please refer to the documentation.

Found a mistake? Update the entry.
Share this article: