My Library Docs

Multi-root Sidebar

flex-layout

  • Getting Started

  • Guides

  • Reference

typed-rx-http

  • Getting Started

  • Guides

  • Reference

global-rx-state

  • Getting Started

  • Guides

  • Reference

webflux-fe-dev-assistant

reactive-mongo-dsl

Overview

A small query helper for RDBMS developers new to MongoDB has grown to meet the demands of real projects, explaining the history and design criteria that led to the Driver-first MongoDB convenience layer in 1.0.0.


Origin story

I developed primarily around RDBMS for about four years and was mostly familiar with AWS RDS in production environments. When starting a new personal project, I chose MongoDB for the first time and realized it was not just a database with flexible schemas, but a tool that could significantly change my development productivity.

Especially MongoDB Atlas felt like a new world to me. I could use specialized search and vector search within a single data platform, which I previously thought required separate systems, and operational tasks like backup, monitoring, and scaling were much more convenient compared to my experience with AWS RDS. While I could separate search engines or vector databases as separate services if more specialized scale and functionality were needed, the range that could be solved with just Atlas was very appealing during the phase of quickly building and operating a project.

The problem was that writing dynamic queries was too slow for me as a newcomer to MongoDB. Initially, the findBy... form of the repository was sufficient, but as the search conditions became more complex, I had to assemble Query, Criteria, and operator expressions manually. In an unfamiliar state, assembling repetitive conditions was prone to typos, and continuously writing the same type of code significantly slowed down development.

Started with a simple DSL for queries

So the first thing I created was a small class to simplify the R in CRUD. This was the starting point of what is now known as ReactiveMongoDsl. At that time, there were no features like aggregation, pipeline, or lookup, and it was just a bit faster assembly of Query, Criteria, and paging from Spring Data ReactiveMongoTemplate. I thought the remaining tasks could be sufficiently handled using the basic ReactiveMongoTemplate or the MongoDB Driver directly.

However, a developer I was working with had a different perspective. He was also new to MongoDB and would ask if the DSL I created had the necessary features like upsert or bulk whenever those tasks were needed. While I could have told him to use the basic API directly, I thought he would face the same difficulties I experienced when learning MongoDB for the first time. So I started adding features to ReactiveMongoDsl one by one as they became necessary.

Initially, it was about 600 lines, and even when it reached 1,000 lines, I thought there was no need to split it into multiple classes. When it got to around 2,000 lines, I pondered a bit, but the structure for separation felt like it would grow even larger, and honestly, I found that task tedious. When it exceeded 5,000 lines, I could no longer postpone it and moved some separable features outside, but I was already in a position where it was burdensome to finely divide the core flow. The reason the core class is large is not because I designed a massive DSL from the beginning, but rather due to the history of continuously stacking the features needed for real projects at the same entry point.

After adding features like querying, aggregation, lookup, atomic updates, bulk, history, Atlas Search, and Vector Search, I found that most of the functionalities I frequently needed when using MongoDB in Java projects were included. Instead of keeping it as an internal helper within a single project, I separated it so that it could be used in the same way across multiple projects, which is the birth process of reactive-mongo-dsl.

From Spring helper to Driver-first library

The starting point was a helper to make using ReactiveMongoTemplate more convenient, but as features grew and it began to be reused across multiple projects, I determined that it was inappropriate for the execution model of a specific framework to define the boundaries of the library. The core of 1.0.0 uses MongoExecutionContext as the execution contract and directly utilizes the MongoDB Reactive Streams Driver.

This change is not to exclude Spring Data MongoDB. In Spring applications, the collection naming of ReactiveMongoTemplate, MongoConverter, custom conversion, and reactive auditing can be connected through the MongoExecutionContext adapter. The core is kept independent of the framework, and only the necessary applications continue to use the existing Spring configuration.

At the same time, it became clearer that we would not re-implement features already well provided by the MongoDB Driver within the DSL. General aggregation can receive Bson stages directly, and for Search/Vector, we provide escape hatches like SearchOperator, VectorSearchQuery, driverOptions(...), and stage(Bson). This choice allows users to utilize new features provided by the Driver without waiting for the next DSL release.

Development philosophy

Does not hide MongoDB

I did not want to create an ORM that transforms MongoDB's features into concepts of other databases. Instead, I connect MongoDB's query, aggregation, transaction, Atlas Search, and Vector Search into shorter and more discoverable flows.

Only add features that are actually needed.

This library was not implemented after designing the feature list first. It was necessary to add querying for operational projects, upsert and bulk functionalities, and search capabilities, which led to the addition of Atlas Search and Vector Search.

Prioritizes productivity over structural perfection.

Does not claim that a large core class is an ideal structure. At the start, it was more important for the team to handle unfamiliar MongoDB tasks quickly in the same way, and even now, the actual usage flow takes precedence over the abstraction itself.

Does not recreate what the Driver already does.

If the MongoDB Java Driver has a typed builder, it uses that type as much as possible. While the DSL provides convenience APIs for repetitive combinations that can be meaningfully reduced, it does not duplicate the Driver API under a different name.

Should be able to fall back to the basic API.

Does not believe that the DSL replaces all situations. It provides escape hatches like Bson, Driver filter/sort/operator/options, and publisher customizer, allowing direct use of the MongoDB Driver if needed.

Four flows covered in the current document.

General Mongo Query.

This is the flow where this library started. It constructs conditions and execution in the order of execute* → fields(...) → end() → find/findAll/count/delete/exists/atomicUpdate.

Driver-native Aggregation.

To directly control from the first stage of the pipeline or to use the new aggregation features provided by the Driver, use the aggregation().stage(Bson) flow. Stages already provided by the Driver, like $score and $scoreFusion, can be passed through without re-implementation by the DSL.

Atlas Search

This flow was added while applying Atlas Search to the project without first introducing a separate search service. It constructs text, compound, score, highlight, and sequence tokens after search(index).

Vector Search

This flow was added to connect embedding searches within MongoDB. It configures query vectors or automated embeddings, ANN/ENN, pre/post filters, and nested/array embedding options.

Execution model of version 1.0.0.
text

Separating general queries from Search/Vector is to avoid hiding the actual pipeline constraints of MongoDB. $search and $vectorSearch have first stage constraints, and functionalities that require the caller to control the entire first stage can fall back to aggregation().

Scope used in actual projects

Pass pairs without conditions as null to assemble dynamic search conditions per screen.

Separates data and totalCount with PageStream to maintain bulk processing flow in a reactive publisher state.

Creates safe operations against duplicate execution with atomicUpdate().upsertOne().document().setOnInsert(...).

Stores collected data and external integration data with bulk upsert based on ID or business key.

Returns lookup results and total counts in one pipeline with executeLookupAndCount.

Uses sequence tokens from Atlas Search and automated embedding queries from Vector Search.

New aggregation stages provided by the Driver connect directly with aggregation().stage(Bson) or Search/Vector's stage(Bson).

If atomicity is needed between DSL operations, specify the ClientSession transaction scope with getTxJob(...).

© 2026 Byeolnaerim. All rights reserved.IntroductionPrivacy Policy