eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

eBook – Guide Spring Cloud – NPI (cat=Cloud/Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

1. Overview

Today, cloud-hosted managed databases have become increasingly popular. One such example is Cloud Firestore, a NoSQL document database offered by Firebase and Google, which provides on-demand scalability, flexible data modelling, and offline support for mobile and web applications.

In this tutorial, we’ll explore how to use Cloud Firestore for data persistence in a Spring Boot application. To make our learning more practical, we’ll create a rudimentary task management application that allows us to create, retrieve, update, and delete tasks using Cloud Firestore as the backend database.

2. Cloud Firestore 101

Before diving into the implementation, let’s look at some of the key concepts of Cloud Firestore.

In Cloud Firestore, data is stored in documents, grouped into collections. A collection is a container for documents, and each document contains a set of key-value pairs of varying data structures, like a JSON object.

Cloud Firestore uses a hierarchical naming convention for document paths. A document path consists of a collection name followed by a document ID, separated by a forward slash. For example, tasks/001 represents a document with ID 001 within the tasks collection.

3. Setting up the Project

Before we can start interacting with Cloud Firestore, we’ll need to include an SDK dependency and configure our application correctly.

3.1. Dependencies

Let’s start by adding the Firebase admin dependency to our project’s pom.xml file:

<dependency>
    <groupId>com.google.firebase</groupId>
    <artifactId>firebase-admin</artifactId>
    <version>9.3.0</version>
</dependency>

This dependency provides us with the necessary classes to interact with Cloud Firestore from our application.

3.2. Data Model

Now, let’s define our data model:

class Task {

    public static final String PATH = "tasks";

    private String title;

    private String description;

    private String status;

    private Date dueDate;

    // standard setters and getters

}

The Task class is the central entity in our tutorial, and represents a task in our task management application.

The PATH constant defines the Firestore collection path where we’ll store our task documents.

3.3. Defining Firestore Configuration Bean

Now, to interact with the Cloud Firestore database, we need to configure our private key to authenticate API requests.

For our demonstration, we’ll create the private-key.json file in our src/main/resources directory. However, in production, the private key should be loaded from an environment variable or fetched from a secret management system to enhance security.

We’ll load our private key using the @Value annotation and use it to define our Firestore bean:

@Value("classpath:/private-key.json")
private Resource privateKey;

@Bean
public Firestore firestore() {
    InputStream credentials = new ByteArrayInputStream(privateKey.getContentAsByteArray());
    FirebaseOptions firebaseOptions = FirebaseOptions.builder()
      .setCredentials(GoogleCredentials.fromStream(credentials))
      .build();

    FirebaseApp firebaseApp = FirebaseApp.initializeApp(firebaseOptions);
    return FirestoreClient.getFirestore(firebaseApp);
}

The Firestore class is the main entry point for interacting with the Cloud Firestore database.

4. Setting up Local Test Environment With Testcontainers

To facilitate local development and testing, we’ll use the GCloud module of Testcontainers to set up a Cloud Firestore emulator. For this, we’ll add its dependency to our pom.xml file:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>gcloud</artifactId>
    <version>1.20.1</version>
    <scope>test</scope>
</dependency>

The prerequisite for running the Firestore emulator via Testcontainers is an active Docker instance.

Once we’ve added the required dependency, we’ll create a @TestConfiguration class that defines a new Firestore bean:

private static FirestoreEmulatorContainer firestoreEmulatorContainer = new FirestoreEmulatorContainer(
    DockerImageName.parse("gcr.io/google.com/cloudsdktool/google-cloud-cli:488.0.0-emulators")
);

@TestConfiguration
static class FirestoreTestConfiguration {

    @Bean
    public Firestore firestore() {
        firestoreEmulatorContainer.start();
        FirestoreOptions options = FirestoreOptions
          .getDefaultInstance()
          .toBuilder()
          .setProjectId(RandomString.make().toLowerCase())
          .setCredentials(NoCredentials.getInstance())
          .setHost(firestoreEmulatorContainer.getEmulatorEndpoint())
          .build();
        return options.getService();
    }

}

We use the Google Cloud CLI Docker image to create a container of our emulator. Then inside our firestore() bean method, we start the container and configure our Firestore bean to connect to the emulator endpoint.

This setup allows us to spin up a throwaway instance of the Cloud Firestore emulator and have our application connect to it instead of the actual Cloud Firestore database.

5. Performing CRUD Operations

With our test environment set up, let’s explore how to perform CRUD operations on our Task data model.

5.1. Creating Documents

Let’s start by creating a new task document:

Task task = Instancio.create(Task.class);

DocumentReference taskReference = firestore
  .collection(Task.PATH)
  .document();
taskReference.set(task);

String taskId = taskReference.getId();
assertThat(taskId).isNotBlank();

We use Instancio to create a new Task object with random test data. Then we call the document() method on our tasks collection to obtain a DocumentReference object, which represents the document’s location in the Cloud Firestore database. Finally, we set the task data on our DocumentReference object to create a new task document.

When we invoke the document() method without any arguments, Firestore auto-generates a unique document ID for us. We can retrieve this auto-generated ID using the getId() method.

Alternatively, we can create a task document with a custom ID:

Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);

firestore
  .collection(Task.PATH)
  .document(taskId)
  .set(task);

Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
    DocumentSnapshot taskSnapshot = firestore
      .collection(Task.PATH)
      .document(taskId)
      .get().get();
    assertThat(taskSnapshot.exists())
      .isTrue();
});

Here, we generate a random taskId and pass it to the document() method to create a new task document against it. We then use Awaitility to wait for the document to be created and assert its existence.

5.2. Retrieving and Querying Documents

Although we’ve indirectly looked at how to retrieve a task document by its ID in the previous section, let’s take a closer look:

Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);
// ... save task in Firestore

DocumentSnapshot taskSnapshot = firestore
  .collection(Task.PATH)
  .document(taskId)
  .get().get();

Task retrievedTask = taskSnapshot.toObject(Task.class);
assertThat(retrievedTask)
  .usingRecursiveComparison()
  .isEqualTo(task);

To retrieve our task document, we call the get() method on the DocumentReference object. This method returns an ApiFuture<DocumentSnapshot>, representing an asynchronous operation. To block and wait for the operation to complete, we call the get() method again on the returned future, which gives us a DocumentSnapshot object.

To convert the DocumentSnapshot object into a Task object, we use the toObject() method.

Furthermore, we can also query documents based on specific conditions:

// Set up test data
Task completedTask = Instancio.of(Task.class)
  .set(field(Task::getStatus), "COMPLETED")
  .create();
Task inProgressTask = // ... task with status IN_PROGRESS
Task anotherCompletedTask = // ... task with status COMPLETED
List<Task> tasks = List.of(completedTask, inProgressTask, anotherCompletedTask);
// ... save all the tasks in Firestore

// Retrieve completed tasks
List<QueryDocumentSnapshot> retrievedTaskSnapshots = firestore
  .collection(Task.PATH)
  .whereEqualTo("status", "COMPLETED")
  .get().get().getDocuments();

// Verify only matching tasks are retrieved
List<Task> retrievedTasks = retrievedTaskSnapshots
  .stream()
  .map(snapshot -> snapshot.toObject(Task.class))
  .toList();
assertThat(retrievedTasks)
  .usingRecursiveFieldByFieldElementComparator()
  .containsExactlyInAnyOrder(completedTask, anotherCompletedTask);

In our above example, we create multiple task documents with different status values and save them to our Cloud Firestore database. We then use the whereEqualTo() method to retrieve only the task documents with a COMPLETED status.

Additionally, we can combine multiple query conditions:

List<QueryDocumentSnapshot> retrievedTaskSnapshots = firestore
  .collection(Task.PATH)
  .whereEqualTo("status", "COMPLETED")
  .whereGreaterThanOrEqualTo("dueDate", Date.from(Instant.now()))
  .whereLessThanOrEqualTo("dueDate", Date.from(Instant.now().plus(7, ChronoUnit.DAYS)))
  .get().get().getDocuments();

Here, we query for all COMPLETED tasks with the dueDate value within the next 7 days.

5.3. Updating Documents

To update a document in Cloud Firestore, we follow a similar process to creating one. If the specified document ID doesn’t exist, Cloud Firestore creates a new document; otherwise, it updates the existing document:

// Save initial task in Firestore
String taskId = Instancio.create(String.class);
Task initialTask = Instancio.of(Task.class)
  .set(field(Task::getStatus), "IN_PROGRESS")
  .create();
firestore
  .collection(Task.PATH)
  .document(taskId)
  .set(initialTask);

// Update the task
Task updatedTask = initialTask;
updatedTask.setStatus("COMPLETED");
firestore
  .collection(Task.PATH)
  .document(taskId)
  .set(initialTask);

// Verify the task was updated correctly
Task retrievedTask = firestore
  .collection(Task.PATH)
  .document(taskId)
  .get().get()
  .toObject(Task.class);
assertThat(retrievedTask)
  .usingRecursiveComparison()
  .isNotEqualTo(initialTask)
  .ignoringFields("status")
  .isEqualTo(initialTask);

We first create a new task document with an IN_PROGRESS status. We then update its status to COMPLETED by calling the set() method again with the updated Task object. Finally, we fetch the document from the database and verify the changes were applied correctly.

5.4. Deleting Documents

Finally, let’s take a look at how we can delete our task documents:

// Save task in Firestore
Task task = Instancio.create(Task.class);
String taskId = Instancio.create(String.class);
firestore
  .collection(Task.PATH)
  .document(taskId)
  .set(task);

// Ensure the task is created
Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
    DocumentSnapshot taskSnapshot = firestore
      .collection(Task.PATH)
      .document(taskId)
      .get().get();
    assertThat(taskSnapshot.exists())
      .isTrue();
});

// Delete the task
firestore
  .collection(Task.PATH)
  .document(taskId)
  .delete();

// Assert that the task is deleted
Awaitility.await().atMost(3, TimeUnit.SECONDS).untilAsserted(() -> {
    DocumentSnapshot taskSnapshot = firestore
      .collection(Task.PATH)
      .document(taskId)
      .get().get();
    assertThat(taskSnapshot.exists())
      .isFalse();
});

Here, we first create a new task document and ensure its existence. We then call the delete() method on the DocumentReference object to delete our task and verify that the document no longer exists.

6. Conclusion

In this article, we’ve explored using Cloud Firestore for data persistence in a Spring Boot application.

We walked through the necessary configurations, including setting up a local test environment using Testcontainers, and performed CRUD operations on our task data model.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)
eBook – eBook Guide Spring Cloud – NPI (cat=Cloud/Spring Cloud)