As we continue to develop and deploy a growing number of microservices, one critical consideration is the choice of data storage. While traditional relational databases such as PostgreSQL, MS SQL Server, and MySQL offer robust capabilities, they can become expensive and complex to scale in serverless environments like Google Cloud Run, AWS Lambda, or Azure Functions.
Serverless architectures offer inherent scalability, and managing relational database scaling in such environments often introduces additional operational overhead and cost. That said, the decision to use a relational database (RDBMS) versus a NoSQL solution should always be driven by the specific business and data requirements.
In this blog post, we explore Google Cloud Firestore, a fully managed, serverless NoSQL document database built for automatic scaling, high performance, and ease of application development. Firestore is particularly well-suited for modern applications, especially those involving AI and ML workloads.
Firestore integrates seamlessly with tools like LangChain and LlamaIndex, supporting patterns such as:
Document loaders for managing and storing structured content,
Vector stores for similarity and semantic search,
Memory modules for use cases like chat history retention.
Additionally, Firestore provides out-of-the-box extensions that simplify integration with popular AI services, enabling features such as automated embedding generation, language translation, and image classification with minimal configuration.
In this article, we'll walk through how to use Firestore on the server side, using Micronaut and Java 21 as our development stack. Let’s dive into how Firestore can help you build scalable, AI-powered applications with ease.
OOPS, we’ve had enough theory—let’s dive into some code and see how Micronaut can help us integrate with Firestore and build a robust document storage solution.
Prerequisites
- Google Cloud project with Firestore enabled.
- Micronaut CLI or a Micronaut project setup.
- Google Cloud SDK and credentials for Firestore access.
- Java 21 and Maven/Gradle for dependency management.
Getting Started with Firestore in Micronaut (Java 21)
Micronaut is a modern, lightweight framework optimized for building microservices and serverless applications with fast startup time and low memory usage—ideal for cloud-native deployments.
Step 1: Bootstrap the Project
Head over to the Micronaut Launch site or use the Micronaut CLI to generate your project:
mn create-app \
--build=gradle_kotlin \
--jdk=21 \
--lang=java \
--test=junit \
--features=micronaut-aot,graalvm,management,gcp-logging \
san.jaisy.firestore
Let’s break down the features we’ve included:
micronaut-aot: Enables ahead-of-time (AOT) compilation for faster startup and smaller footprint—great for serverless.
graalvm: Adds support for native image generation with GraalVM.
management: Provides production-ready features such as health checks and metrics.
gcp-logging: Integrates with Google Cloud Logging for easy observability in GCP environments.
Congratulations! You’ve just generated a project that’s cloud-native, AI-friendly, and 100% cooler than other framwork.
Now let’s put that shiny new app to work and get Firestore talking to it—like two old friends reconnecting over JSON.
Step 2: Client libraries
Set up Application Default Credentials (ADC) in your local environment:
gcloud init
If you're using a local shell, then create local authentication credentials for your user account:
gcloud auth application-default login
NOTE: You don't need to do this if you're using Cloud Shell.
Step 3: Exploring the build.gradle.kts File
Once you've generated your Micronaut project, the next stop is your build.gradle.kts file. Here's what it should look like by default:
plugins {
id("io.micronaut.application") version "4.5.3"
id("com.gradleup.shadow") version "8.3.6"
id("io.micronaut.aot") version "4.5.3"
}
version = "0.1"
group = "san.jaisy"
repositories {
mavenCentral()
}
dependencies {
annotationProcessor("io.micronaut:micronaut-http-validation")
annotationProcessor("io.micronaut.serde:micronaut-serde-processor")
implementation("io.micronaut:micronaut-management")
implementation("io.micronaut.gcp:micronaut-gcp-logging")
implementation("io.micronaut.serde:micronaut-serde-jackson")
compileOnly("io.micronaut:micronaut-http-client")
runtimeOnly("ch.qos.logback:logback-classic")
testImplementation("io.micronaut:micronaut-http-client")
}
application {
mainClass = "san.jaisy.Application"
}
java {
sourceCompatibility = JavaVersion.toVersion("21")
targetCompatibility = JavaVersion.toVersion("21")
}
graalvmNative.toolchainDetection = false
micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("san.jaisy.*")
}
aot {
// Please review carefully the optimizations enabled below
// Check https://micronaut-projects.github.io/micronaut-aot/latest/guide/ for more details
optimizeServiceLoading = false
convertYamlToJava = false
precomputeOperations = true
cacheEnvironment = true
optimizeClassLoading = true
deduceEnvironment = true
optimizeNetty = true
replaceLogbackXml = true
}
}
tasks.named<io.micronaut.gradle.docker.NativeImageDockerfile>("dockerfileNative") {
jdkVersion = "21"
}
Step 4: Wait a Minute… Where’s Firestore?
If you’ve been following along, you might have noticed that while we've set up a solid foundation, we haven’t actually added Firestore yet. 😅
Let’s fix that.
implementation("com.google.cloud:google-cloud-firestore:3.31.6")
🎉 And just like that, your app is Firestore-ready... or is it?
Well, not quite. While the library is in place, we still need to configure the Firestore client, authenticate with GCP, and write some actual code to make it do something meaningful.
Step 5: Coding time 👨💻👩💻 👨💻👩💻 🔥🔥 🎉🎉
📡 Controller: Exposing the Feedback API
@Controller("v1/firestore")
@ExecuteOn(TaskExecutors.BLOCKING)
public class FirestoreController {
private final IFirestoreService firestoreService;
public OutlookController(IFirestoreService firestoreService) {
this.firestoreService = firestoreService;
}
@Post("/feedback")
public HttpResponse<?> feedback(@Valid @Body UserFeedbackRequest request) {
var result = firestoreService.create(request,httpRequest);
return result.match(success -> HttpResponse.ok(result.value), () -> HttpResponse.serverError());
}
}
📝 This controller exposes a POST /v1/firestore/feedback endpoint to accept user feedback.
🧾 DTO: Validating Feedback Requests
@Serdeable
public record UserFeedbackRequest(@NotNull String requestId,
@NotNull String itemId,
@Size(min = 10) String feedback,
FeedbackType feedbackType) {
}
🎯 This record handles incoming feedback. We apply validation annotations (@NotNull, @Size) so invalid data is caught before hitting the database.
🏭 Factory: Configuring Firestore Client
@Factory
public class FirestoreFactory {
@Singleton
public Firestore firestore(GcpConfig gcpConfig) throws IOException {
return FirestoreOptions.getDefaultInstance().toBuilder()
.setProjectId(gcpConfig.project())
.setCredentials(GoogleCredentials.getApplicationDefault())
.build()
.getService();
}
}
🔧 This factory sets up a singleton Firestore client using the FirestoreOptions builder.
🧩 Interface: Contract for the Firestore Service
public sealed interface IFirestoreService permits FirestoreService {
Result<Response<UserFeedbackRequest>> create(UserFeedbackRequest userFeedbackRequest, HttpRequest<?> httpRequest);
}
💾 Service: Writing Data to Firestore
@Singleton
public record FirestoreService(Firestore firestore) implements IFirestoreService {
private static final Logger LOG = Logger.getLogger(FirestoreService.class.getName());
@Override
public Result<Response<UserFeedbackRequest>> create(UserFeedbackRequest request) {
// Store all feedback in the same top-level collection
DocumentReference docRef = firestore.collection("firestore").document("feedback");
Map<String, Object> data = new HashMap<>();
data.put("RequestId", requestId);
data.put("ItemId", request.itemId());
data.put("Comments", request.feedback());
data.put("CreatedDate", Instant.now().toString());
data.put("Type", request.feedbackType().name);
try {
var result = docRef.set(data);
var writtenResponse = result.get();
LOG.info("Write result updated time : " + writtenResponse.getUpdateTime());
} catch (InterruptedException | ExecutionException e) {
LOG.severe(e.getMessage());
return Result.of(new Exception(e));
}
return Result.of(new Response<>(request, MetaResponse.Of(httpRequest)));
}
}
🧠 This is where the real magic happens. We take the incoming request, transform it into a Firestore document, and save it under the "firestore/feedback" path. Logging ensures we know when the document was successfully written.
💡 Pro tip: You can dynamically generate document IDs with .document() or let Firestore auto-generate them using .add().
🎭
At this point, Firestore and Micronaut are basically best friends—trading JSON over a coffee and laughing at how easy it was to build this.
If bugs show up now, they’re probably just jealous they didn’t get invited to the architecture meeting.
✅ Conclusion: Build Smart, Build Serverless
Firestore proves to be an excellent choice when building scalable, event-driven, and AI-powered microservices in a serverless environment. With its seamless integration with Micronaut, automatic scaling, and native support for real-time data and AI tooling, you can focus on delivering business value instead of wrestling with infrastructure.
So the next time someone asks, “SQL or NoSQL?” — you can confidently say:
“It depends... but if you're building serverless and smart, Firestore’s got your back.” 🚀👨💻👩💻✨
Hey everyone! We’re launching DEV Contributor rewards for all verified Dev.to authors. Click here here to see if you qualify (instant distribution). – Admin