Kotlin, Quarkus, and MongoDB — do these three technologies work well together?
In this tutorial, we’ll build a simple application that connects to MongoDB using the Panache pattern and performs aggregation queries.
Along the way, we’ll set up the project, explore the structure, and run a basic aggregation pipeline to query a movies dataset.
Prerequisites
- Basic knowledge of Kotlin
- Maven installed
- A MongoDB instance (Atlas or local)
- Docker installed and running (optional, if you want to use the local lab setup)
What we will build
We’re going to build a simple application that connects to a database containing around 21,000 movies.
Each movie document has a structure similar to this:
{
"title": "The Great Train Robbery",
"fullplot": "Among the earliest existing films in American cinema...",
"plot": "A group of bandits stage a brazen train hold-up, only to find a determined posse hot on their heels.",
"genres": [
"Short",
"Western"
],
"cast": [
"A.C. Abadie",
"Gilbert M. 'Broncho Billy' Anderson",
"George Barnes",
"Justus D. Barnes"
],
...
}
Our goal is to create an aggregation that selects cast members and counts how many movies each one appears in, returning results like this:
{
"_id": "Gérard Depardieu",
"totalMovies": 67
},
{
"_id": "Robert De Niro",
"totalMovies": 58
}
Setting up MongoDB (Optional)
To get started, we first need to set up a MongoDB connection.
You have a couple of options:
- Create a free MongoDB Atlas account and set up a cluster by following this tutorial
- Or use a local MongoDB instance To make things easier, you can use this CLI tool to spin up a local MongoDB environment with sample data already loaded:
1 - Install the cli:
npm install -g @ricardohsmello/mongodb-cli-lab
2 - Create the cluster:
mongodb-cli-lab up --topology standalone --mongodb-version 8.2 --port 28000 --sample-databases sample_mflix
This is a community-driven initiative and not an official MongoDB product.
It is intended for development, demos, and learning purposes only.
Not intended for production use.
This command starts a local MongoDB instance and automatically loads the sample_mflix dataset, which includes the movies collection we’ll use in this tutorial.
Once your MongoDB instance is up and running, make sure you have access to the movies collection, as we’ll use it to build our aggregation queries.
Creating the Quarkus project
Now that our database is ready, let’s create our Quarkus application.
We’ll use the Quarkus Maven plugin to bootstrap the project with the required extensions:
mvn io.quarkus.platform:quarkus-maven-plugin:3.34.1:create \
-DprojectGroupId=com.example.mongodb.lab \
-DprojectArtifactId=kotlin-quarkus-lab \
-Dextensions='kotlin,rest-jackson, quarkus-mongodb-panache-kotlin'
Here, we are including a few important extensions:
- Kotlin – to write our application using Kotlin
- REST (Jackson) – to expose HTTP endpoints and handle JSON
- MongoDB Panache – to simplify data access and interactions with MongoDB
Once the project is created, open it in your favorite IDE to continue.
Configuring the MongoDB Connection
With the project open in your IDE, the first step is to configure the connection to MongoDB.
To do that, open the application.properties file and add the following properties:
%dev.quarkus.mongodb.connection-string=mongodb://localhost:28000
%dev.quarkus.mongodb.database=sample_mflix
These settings configure the application to connect to our local MongoDB instance and use the sample_mflix database.
The %dev prefix means that these properties will only be applied when running the application in development mode.
Defining the Movie entity
Now, we need to create a class to represent our movies collection.
Using Panache, we can map this collection to a Kotlin class Movie.kt, like this:
import io.quarkus.mongodb.panache.common.MongoEntity
@MongoEntity(collection = "movies")
class Movie {
lateinit var title: String
var plot: String? = null
var cast: List<String>? = null
}
The @MongoEntity annotation tells Quarkus which collection this class maps to.
Here, title is defined as a required field using lateinit, since we expect every document to have it.
On the other hand, plot and cast are optional fields, so we declare them as nullable (?) and initialize them with null. This allows the application to handle documents where these fields might be missing.
Creating the Repository
Next, we’ll create a repository to interact with MongoDB using Panache.
import io.quarkus.mongodb.panache.kotlin.PanacheMongoRepository
import jakarta.enterprise.context.ApplicationScoped
@ApplicationScoped
class MovieRepository : PanacheMongoRepository<Movie> {
}
By extending PanacheMongoRepository, we get access to built-in methods to interact with the database, such as queries and data operations.
This is where we’ll implement our aggregation logic.
Exposing the repository through a resource
Next, we’ll interact with this repository through a REST resource:
import jakarta.ws.rs.GET
import jakarta.ws.rs.Path
import jakarta.ws.rs.Produces
@Path("/movies")
@Produces("application/json")
class MovieResource(
val repository: MovieRepository
) {
@GET
fun list() = repository.findAll()
.list()
}
This resource exposes a /movies endpoint that returns all documents from the collection.
Running the Application and Testing the Endpoint
Now, let’s run the application and test the endpoint.
To make that easier, I’ll create an HTTP file called movie.http and add the following request:
### FIND
GET http://localhost:8080/movies
Accept: application/json
Once we run it, we’ll immediately notice the first problem: we are returning all 21,000 movies at once.
That is not a good idea. We should always be careful with how much data we return, both to avoid unnecessary load on the database and to make the API more efficient.
A simple improvement here is pagination.
So let’s go back to the MovieResource class and add two query parameters: page and size.
import jakarta.ws.rs.DefaultValue
import jakarta.ws.rs.QueryParam
@GET
fun list(
@QueryParam("page") @DefaultValue("0") page: Int,
@QueryParam("size") @DefaultValue("20") size: Int,
) = repository.findAll()
.page(page, size)
.list()
With this change, the endpoint will return only 20 results by default.
If we run the request again, we’ll get a much smaller, and more manageable response.
Working with the Aggregation Framework
The MongoDB Aggregation Framework is a powerful feature for transforming and analyzing data directly in the database. Instead of just retrieving documents, we can use aggregation stages to reshape the data, group values, sort results, and answer more interesting questions.
In this example, we want to find the actors who appear most frequently in the dataset. To do that, we’ll use the following aggregation pipeline:
[
{
$unwind:
{
path: "$cast"
}
},
{
$group:
{
_id: "$cast",
totalMovies: {
$sum: 1
}
}
},
{
$sort:
{
totalMovies: -1
}
},
{
$limit: 3
}
]
Let’s break it down:
$unwind takes the cast field, which is an array, and splits it into multiple documents. In simple terms, if a movie has three actors in its cast, it will temporarily become three separate records during this stage, one for each actor.
$group then groups those records by actor name and counts how many times each one appears.
$sort orders the results in descending order, so the most frequent actors appear first.
$limit keeps only the top results.
Implementing the Aggregation in the Repository
Now let’s implement this pipeline inside the MovieRepository:
fun findTopFrequentActors(): List<Document> =
mongoCollection().withDocumentClass(Document::class.java)
.aggregate(
listOf(
Aggregates.unwind("\$cast"),
Aggregates.group(
"\$cast", Accumulators.sum("totalMovies", 1)
),
Aggregates.sort(Sorts.descending("totalMovies")),
Aggregates.limit(3)
)
).toList()
Here, we are using the underlying MongoDB collection and defining the aggregation stages with the MongoDB Java driver API.
Since the result of this pipeline does not map directly to our Movie class, returning a Document is a simple and practical choice.
Next, let’s expose this aggregation through a new endpoint in our MovieResource:
@GET
@Path("/top-frequent-actors")
fun findTopFrequentActors() = repository.findTopFrequentActors()
With this, our application now provides a dedicated endpoint to return the actors who appear in the highest number of movies.
### TOP FREQUENT ACTORS
GET http://localhost:8080/movies/top-frequent-actors
Accept: application/json
Once you run it, you should see a response similar to this:
[
{
"_id": "Gèrard Depardieu",
"totalMovies": 67
},
{
"_id": "Robert De Niro",
"totalMovies": 58
},
{
"_id": "Michael Caine",
"totalMovies": 51
}
]
And that’s it! We now have a working aggregation pipeline in a Kotlin application using Quarkus, Panache, and MongoDB.
Conclusion
Yes — Kotlin, Quarkus, and MongoDB work very well together in a practical and efficient way.
In this tutorial, we saw how to quickly build an application, map a collection using Panache, and leverage the MongoDB Aggregation Framework to work with data in a powerful yet simple way.
If you’d like to explore further, the full source code is available on GitHub.
Top comments (0)