diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 56a4abb8a..6fc3843bd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -174,6 +174,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ flamingock-mongodb-reactive-auditstore:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: flamingock-mongodb-reactive-auditstore
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
flamingock-couchbase-auditstore:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -279,6 +292,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ flamingock-mongodb-reactive-externalsystem-api:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: flamingock-mongodb-reactive-externalsystem-api
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
flamingock-mongodb-sync-targetsystem:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -292,6 +318,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ flamingock-mongodb-reactive-targetsystem:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: flamingock-mongodb-reactive-targetsystem
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
flamingock-mongodb-springdata-targetsystem:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -305,6 +344,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ flamingock-mongodb-springdata-reactive-targetsystem:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: flamingock-mongodb-springdata-reactive-targetsystem
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
flamingock-sql-externalsystem-api:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -396,6 +448,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ flamingock-reactive-util:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: flamingock-reactive-util
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
mongodb-util:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -409,6 +474,32 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ mongodb-sync-util:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: mongodb-sync-util
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
+ mongodb-reactive-util:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: mongodb-reactive-util
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
mongodb-test-kit:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -422,6 +513,19 @@ jobs:
FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+ mongodb-reactive-test-kit:
+ needs: [ build ]
+ uses: ./.github/workflows/module-release-graalvm.yml
+ with:
+ module: mongodb-reactive-test-kit
+ secrets:
+ FLAMINGOCK_JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }}
+ FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }}
+ FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }}
+ FLAMINGOCK_JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }}
+
dynamodb-util:
needs: [ build ]
uses: ./.github/workflows/module-release-graalvm.yml
@@ -585,14 +689,18 @@ jobs:
flamingock-community,
flamingock-community-bom,
flamingock-mongodb-sync-auditstore,
+ flamingock-mongodb-reactive-auditstore,
flamingock-couchbase-auditstore,
flamingock-dynamodb-auditstore,
flamingock-importer,
flamingock-springboot-integration,
flamingock-nontransactional-targetsystem,
flamingock-mongodb-externalsystem-api,
+ flamingock-mongodb-reactive-externalsystem-api,
flamingock-mongodb-sync-targetsystem,
+ flamingock-mongodb-reactive-targetsystem,
flamingock-mongodb-springdata-targetsystem,
+ flamingock-mongodb-springdata-reactive-targetsystem,
flamingock-sql-externalsystem-api,
flamingock-sql-targetsystem,
flamingock-dynamodb-externalsystem-api,
@@ -600,8 +708,12 @@ jobs:
flamingock-couchbase-externalsystem-api,
flamingock-couchbase-targetsystem,
test-util,
+ flamingock-reactive-util,
mongodb-util,
+ mongodb-sync-util,
+ mongodb-reactive-util,
mongodb-test-kit,
+ mongodb-reactive-test-kit,
dynamodb-util,
dynamodb-test-kit,
couchbase-util,
diff --git a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts
index c7d1a8598..6a1973de3 100644
--- a/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts
+++ b/buildSrc/src/main/kotlin/flamingock.project-structure.gradle.kts
@@ -24,6 +24,7 @@ val communityProjects = setOf(
"flamingock-community-bom",
"flamingock-community",
"flamingock-mongodb-sync-auditstore",
+ "flamingock-mongodb-reactive-auditstore",
"flamingock-couchbase-auditstore",
"flamingock-dynamodb-auditstore",
"flamingock-sql-auditstore",
@@ -38,7 +39,9 @@ val pluginProjects = setOf(
val targetSystemProjects = setOf(
"flamingock-nontransactional-targetsystem",
"flamingock-mongodb-sync-targetsystem",
+ "flamingock-mongodb-reactive-targetsystem",
"flamingock-mongodb-springdata-targetsystem",
+ "flamingock-mongodb-springdata-reactive-targetsystem",
"flamingock-sql-targetsystem",
"flamingock-dynamodb-targetsystem",
"flamingock-couchbase-targetsystem"
@@ -46,6 +49,7 @@ val targetSystemProjects = setOf(
val externalSystemProjects = setOf(
"flamingock-mongodb-externalsystem-api",
+ "flamingock-mongodb-reactive-externalsystem-api",
"flamingock-couchbase-externalsystem-api",
"flamingock-dynamodb-externalsystem-api",
"flamingock-sql-externalsystem-api"
@@ -55,6 +59,9 @@ val utilProjects = setOf(
"general-util",
"test-util",
"mongodb-util",
+ "mongodb-sync-util",
+ "mongodb-reactive-util",
+ "flamingock-reactive-util",
"dynamodb-util",
"couchbase-util"
)
@@ -68,6 +75,7 @@ val legacyProjects = setOf(
val testKitsProjects = setOf(
"mongodb-test-kit",
+ "mongodb-reactive-test-kit",
"dynamodb-test-kit",
"sql-test-kit",
"couchbase-test-kit"
diff --git a/cloud/flamingock-cloud/build.gradle.kts b/cloud/flamingock-cloud/build.gradle.kts
index cad2b6b01..d2432c887 100644
--- a/cloud/flamingock-cloud/build.gradle.kts
+++ b/cloud/flamingock-cloud/build.gradle.kts
@@ -9,12 +9,16 @@ dependencies {
api(project(":core:target-systems:flamingock-couchbase-targetsystem"))
api(project(":core:target-systems:flamingock-dynamodb-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-externalsystem-api"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-springdata-targetsystem"))
+ api(project(":core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-sync-targetsystem"))
api(project(":core:target-systems:flamingock-sql-targetsystem"))
// Community
api(project(":community:flamingock-couchbase-auditstore"))
api(project(":community:flamingock-dynamodb-auditstore"))
+ api(project(":community:flamingock-mongodb-reactive-auditstore"))
api(project(":community:flamingock-mongodb-sync-auditstore"))
api(project(":community:flamingock-sql-auditstore"))
@@ -41,4 +45,4 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
-}
\ No newline at end of file
+}
diff --git a/community/flamingock-community-bom/build.gradle.kts b/community/flamingock-community-bom/build.gradle.kts
index f7a3eac00..feb6aa159 100644
--- a/community/flamingock-community-bom/build.gradle.kts
+++ b/community/flamingock-community-bom/build.gradle.kts
@@ -8,7 +8,9 @@ plugins {
dependencies {
constraints {
// Add constraints for BOM managed modules
+ api("io.flamingock:flamingock-reactive-util:${version}")
api("io.flamingock:flamingock-community:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-auditstore:$version")
api("io.flamingock:flamingock-mongodb-sync-auditstore:$version")
api("io.flamingock:flamingock-mongodb-springdata-auditstore:${version}")
api("io.flamingock:flamingock-couchbase-auditstore:$version")
@@ -35,6 +37,9 @@ dependencies {
//mongodb
api("io.flamingock:flamingock-mongodb-sync-template:${mongodbTemplateVersion}")
api("io.flamingock:flamingock-mongodb-externalsystem-api:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-externalsystem-api:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-targetsystem:${version}")
+ api("io.flamingock:flamingock-mongodb-springdata-reactive-targetsystem:${version}")
api("io.flamingock:flamingock-mongodb-sync-targetsystem:${version}")
api("io.flamingock:flamingock-mongodb-springdata-targetsystem:${version}")
diff --git a/community/flamingock-community/build.gradle.kts b/community/flamingock-community/build.gradle.kts
index fd6b77f97..16413ac7f 100644
--- a/community/flamingock-community/build.gradle.kts
+++ b/community/flamingock-community/build.gradle.kts
@@ -8,12 +8,16 @@ dependencies {
api(project(":core:target-systems:flamingock-couchbase-targetsystem"))
api(project(":core:target-systems:flamingock-dynamodb-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-externalsystem-api"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem"))
+ api(project(":core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-springdata-targetsystem"))
api(project(":core:target-systems:flamingock-mongodb-sync-targetsystem"))
api(project(":core:target-systems:flamingock-sql-targetsystem"))
// Community
api(project(":community:flamingock-couchbase-auditstore"))
api(project(":community:flamingock-dynamodb-auditstore"))
+ api(project(":community:flamingock-mongodb-reactive-auditstore"))
api(project(":community:flamingock-mongodb-sync-auditstore"))
api(project(":community:flamingock-sql-auditstore"))
}
@@ -24,4 +28,4 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
-}
\ No newline at end of file
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts b/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts
new file mode 100644
index 000000000..e6315ed76
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/build.gradle.kts
@@ -0,0 +1,33 @@
+import org.jetbrains.kotlin.gradle.utils.extendsFrom
+
+dependencies {
+ implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:mongodb-reactive-util"))
+ implementation(project(":utils:flamingock-reactive-util"))
+ implementation(project(":core:flamingock-core"))
+
+ api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api"))
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0")
+
+ testImplementation(project(":utils:test-util"))
+ testImplementation(project(":utils:mongodb-reactive-test-kit"))
+ testImplementation(project(":core:target-systems:flamingock-nontransactional-targetsystem"))
+ testImplementation(project(":core:target-systems:flamingock-mongodb-reactive-targetsystem"))
+ testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2")
+ testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2")
+ testImplementation("org.mockito:mockito-inline:4.11.0")
+ testImplementation("io.mongock:mongock-standalone:5.5.0")
+}
+
+description = "MongoDB audit store implementation using reactive streams MongoDB driver"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java
new file mode 100644
index 000000000..52e6d6f84
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStore.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.common.core.error.FlamingockException;
+import io.flamingock.internal.core.configuration.community.CommunityConfigurable;
+import io.flamingock.internal.core.external.store.CommunityAuditStore;
+import io.flamingock.internal.core.external.store.audit.community.CommunityAuditPersistence;
+import io.flamingock.internal.core.external.store.lock.community.CommunityLockService;
+import io.flamingock.internal.util.Constants;
+import io.flamingock.internal.util.TimeService;
+import io.flamingock.internal.util.id.RunnerId;
+import io.flamingock.store.mongodb.reactive.internal.MongoDBReactiveAuditPersistence;
+import io.flamingock.store.mongodb.reactive.internal.MongoDBReactiveLockService;
+
+import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME;
+import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME;
+
+public class MongoDBReactiveAuditStore implements CommunityAuditStore {
+
+ private final MongoDBReactiveExternalSystem mongoDBTargetSystem;
+
+ protected RunnerId runnerId;
+ private CommunityConfigurable communityConfiguration;
+ private MongoDBReactiveAuditPersistence persistence;
+ private MongoDBReactiveLockService lockService;
+ private MongoDatabase database;
+ private String auditRepositoryName = DEFAULT_AUDIT_STORE_NAME;
+ private String lockRepositoryName = DEFAULT_LOCK_STORE_NAME;
+ private ReadConcern readConcern = ReadConcern.MAJORITY;
+ private ReadPreference readPreference = ReadPreference.primary();
+ private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true);
+ private boolean autoCreate = true;
+
+ private MongoDBReactiveAuditStore(MongoDBReactiveExternalSystem mongoDBTargetSystem) {
+ this.mongoDBTargetSystem = mongoDBTargetSystem;
+ }
+
+ /**
+ * Creates a {@link MongoDBReactiveAuditStore} using the same MongoDB client and
+ * database configured in the given {@link MongoDBReactiveExternalSystem}.
+ *
+ * Only the underlying MongoDB instance (client + database name) is reused.
+ * No additional target-system configuration is carried over.
+ *
+ * @param targetSystem the target system from which to derive the client and database
+ * @return a new audit store bound to the same MongoDB instance as the target system
+ */
+ public static MongoDBReactiveAuditStore from(MongoDBReactiveExternalSystem targetSystem) {
+ return new MongoDBReactiveAuditStore(targetSystem);
+ }
+
+ @Override
+ public String getId() {
+ return Constants.DEFAULT_MONGODB_AUDIT_STORE;
+ }
+
+ public MongoDBReactiveAuditStore withAuditRepositoryName(String auditRepositoryName) {
+ this.auditRepositoryName = auditRepositoryName;
+ return this;
+ }
+
+ public MongoDBReactiveAuditStore withLockRepositoryName(String lockRepositoryName) {
+ this.lockRepositoryName = lockRepositoryName;
+ return this;
+ }
+
+ public MongoDBReactiveAuditStore withReadConcern(ReadConcern readConcern) {
+ this.readConcern = readConcern;
+ return this;
+ }
+
+ public MongoDBReactiveAuditStore withReadPreference(ReadPreference readPreference) {
+ this.readPreference = readPreference;
+ return this;
+ }
+
+ public MongoDBReactiveAuditStore withWriteConcern(WriteConcern writeConcern) {
+ this.writeConcern = writeConcern;
+ return this;
+ }
+
+ public MongoDBReactiveAuditStore withAutoCreate(boolean autoCreate) {
+ this.autoCreate = autoCreate;
+ return this;
+ }
+
+ @Override
+ public void initialize(ContextResolver baseContext) {
+ runnerId = baseContext.getRequiredDependencyValue(RunnerId.class);
+ communityConfiguration = baseContext.getRequiredDependencyValue(CommunityConfigurable.class);
+ database = mongoDBTargetSystem.getMongoDatabase();
+ this.validate();
+ }
+
+ @Override
+ public synchronized CommunityAuditPersistence getPersistence() {
+ if (persistence == null) {
+ persistence = new MongoDBReactiveAuditPersistence(
+ communityConfiguration,
+ database,
+ auditRepositoryName,
+ readConcern,
+ readPreference,
+ writeConcern,
+ autoCreate
+ );
+ persistence.initialize(runnerId);
+ }
+ return persistence;
+ }
+
+ @Override
+ public synchronized CommunityLockService getLockService() {
+ if (lockService == null) {
+ lockService = new MongoDBReactiveLockService(
+ database,
+ lockRepositoryName,
+ readConcern,
+ readPreference,
+ writeConcern,
+ TimeService.getDefault()
+ );
+ lockService.initialize(autoCreate);
+
+ }
+ return lockService;
+ }
+
+ private void validate() {
+
+ if (database == null) {
+ throw new FlamingockException("The 'database' instance is required.");
+ }
+
+ if (auditRepositoryName == null || auditRepositoryName.trim().isEmpty()) {
+ throw new FlamingockException("The 'auditRepositoryName' property is required.");
+ }
+
+ if (lockRepositoryName == null || lockRepositoryName.trim().isEmpty()) {
+ throw new FlamingockException("The 'lockRepositoryName' property is required.");
+ }
+
+ if (auditRepositoryName.trim().equalsIgnoreCase(lockRepositoryName.trim())) {
+ throw new FlamingockException("The 'auditRepositoryName' and 'lockRepositoryName' properties must not be the same.");
+ }
+
+ if (readConcern == null) {
+ throw new FlamingockException("The 'readConcern' property is required.");
+ }
+
+ if (readPreference == null) {
+ throw new FlamingockException("The 'readPreference' property is required.");
+ }
+
+ if (writeConcern == null) {
+ throw new FlamingockException("The 'writeConcern' property is required.");
+ }
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java
new file mode 100644
index 000000000..9c36202ad
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveConfiguration.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.ReadConcernLevel;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.common.mongodb.MongoDBAuditStoreConfiguration;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+import static com.mongodb.assertions.Assertions.isTrue;
+
+public class MongoDBReactiveConfiguration extends MongoDBAuditStoreConfiguration {
+ private ReadConcernLevel readConcern = ReadConcernLevel.MAJORITY;
+ private WriteConcernLevel writeConcern = WriteConcernLevel.MAJORITY_WITH_JOURNAL;
+ private ReadPreferenceLevel readPreference = ReadPreferenceLevel.PRIMARY;
+
+ public ReadConcernLevel getReadConcern() {
+ return readConcern;
+ }
+
+ public void setReadConcern(ReadConcernLevel readConcern) {
+ this.readConcern = readConcern;
+ }
+
+ public void setReadConcern(String readConcern) {
+ this.readConcern = ReadConcernLevel.fromString(readConcern);
+ }
+
+ public WriteConcernLevel getWriteConcern() {
+ return writeConcern;
+ }
+
+ public void setWriteConcern(WriteConcernLevel writeConcern) {
+ this.writeConcern = writeConcern;
+ }
+
+ public ReadPreferenceLevel getReadPreference() {
+ return readPreference;
+ }
+
+ public void setReadPreference(ReadPreferenceLevel readPreference) {
+ this.readPreference = readPreference;
+ }
+
+ public WriteConcern getBuiltMongoDBWriteConcern() {
+ WriteConcern wc;
+ if (writeConcern.w instanceof String) {
+ wc = new WriteConcern((String) writeConcern.w).withJournal(writeConcern.journal);
+ } else {
+ wc = new WriteConcern((int) writeConcern.w).withJournal(writeConcern.journal);
+ }
+ return writeConcern.getwTimeoutMs() == null
+ ? wc
+ : wc.withWTimeout(writeConcern.getwTimeoutMs().toMillis(), TimeUnit.MILLISECONDS);
+ }
+
+ public void mergeConfig(ContextResolver dependencyContext) {
+ dependencyContext.getPropertyAs("mongodb.autoCreate", Boolean.class)
+ .ifPresent(this::setAutoCreate);
+ dependencyContext.getPropertyAs("mongodb.auditRepositoryName", String.class)
+ .ifPresent(this::setAuditRepositoryName);
+ dependencyContext.getPropertyAs("mongodb.lockRepositoryName", String.class)
+ .ifPresent(this::setLockRepositoryName);
+
+ dependencyContext.getPropertyAs("mongodb.readConcern", String.class)
+ .ifPresent(this::setReadConcern);
+
+ dependencyContext.getPropertyAs("mongodb.writeConcern.w", Object.class)
+ .ifPresent(this.writeConcern::setW);
+ dependencyContext.getPropertyAs("mongodb.writeConcern.journal", Boolean.class)
+ .ifPresent(this.writeConcern::setJournal);
+ dependencyContext.getPropertyAs("mongodb.writeConcern.wTimeout", Duration.class)
+ .ifPresent(this.writeConcern::setwTimeoutMs);
+
+ dependencyContext.getPropertyAs("mongodb.readPreference", ReadPreferenceLevel.class)
+ .ifPresent(this::setReadPreference);
+ }
+
+ public enum ReadPreferenceLevel {
+ PRIMARY(ReadPreference.primary()),
+ PRIMARY_PREFERRED(ReadPreference.primaryPreferred()),
+ SECONDARY(ReadPreference.secondary()),
+ SECONDARY_PREFERRED(ReadPreference.secondaryPreferred()),
+ NEAREST(ReadPreference.nearest());
+
+ private final ReadPreference value;
+
+ ReadPreferenceLevel(ReadPreference value) {
+ this.value = value;
+ }
+
+ public ReadPreference getValue() {
+ return value;
+ }
+ }
+
+ public static class WriteConcernLevel {
+
+ public static final WriteConcernLevel MAJORITY_WITH_JOURNAL = new WriteConcernLevel(
+ WriteConcern.MAJORITY.getWString(),
+ Duration.ofSeconds(1),
+ true);
+ private Object w;
+ private Duration wTimeoutMs;
+ private Boolean journal;
+
+ public WriteConcernLevel(String w, Duration wTimeoutMs, Boolean journal) {
+ this.w = w;
+ this.wTimeoutMs = wTimeoutMs;
+ this.journal = journal;
+ }
+
+ public WriteConcernLevel() {
+ }
+
+ public int getW() {
+ isTrue("w is an Integer", w != null && w instanceof Integer);
+ return (Integer) w;
+ }
+
+ public String getStringW() {
+ isTrue("w is a String", w != null && w instanceof String);
+ return (String) w;
+ }
+
+ public void setW(Object w) {
+ this.w = w;
+ }
+
+ public Duration getwTimeoutMs() {
+ return wTimeoutMs;
+ }
+
+ public void setwTimeoutMs(Duration wTimeoutMs) {
+ this.wTimeoutMs = wTimeoutMs;
+ }
+
+ public Boolean isJournal() {
+ return journal;
+ }
+
+ public void setJournal(Boolean journal) {
+ this.journal = journal;
+ }
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java
new file mode 100644
index 000000000..1e98c2368
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistence.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.internal;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.core.configuration.community.CommunityConfigurable;
+import io.flamingock.internal.core.external.store.audit.community.AbstractCommunityAuditPersistence;
+import io.flamingock.internal.util.Result;
+import io.flamingock.internal.util.id.RunnerId;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MongoDBReactiveAuditPersistence extends AbstractCommunityAuditPersistence {
+
+ private MongoDBReactiveAuditor auditor;
+ private final MongoDatabase database;
+ private final String auditCollectionName;
+ private final ReadConcern readConcern;
+ private final ReadPreference readPreference;
+ private final WriteConcern writeConcern;
+ private final boolean autoCreate;
+
+
+ public MongoDBReactiveAuditPersistence(CommunityConfigurable localConfiguration,
+ MongoDatabase database,
+ String auditCollectionName,
+ ReadConcern readConcern,
+ ReadPreference readPreference,
+ WriteConcern writeConcern,
+ boolean autoCreate) {
+ super(localConfiguration);
+ this.database = database;
+ this.auditCollectionName = auditCollectionName;
+ this.readConcern = readConcern;
+ this.readPreference = readPreference;
+ this.writeConcern = writeConcern;
+ this.autoCreate = autoCreate;
+ }
+
+ @Override
+ protected void doInitialize(RunnerId runnerId) {
+ //Auditor
+ auditor = new MongoDBReactiveAuditor(database, auditCollectionName, readConcern, readPreference, writeConcern);
+ auditor.initialize(autoCreate);
+ }
+
+ @Deprecated
+ @Override
+ public Set> getNonGuardedTypes() {
+ return new HashSet<>(Collections.singletonList(ClientSession.class));
+ }
+
+ @Override
+ public List getAuditHistory() {
+ return auditor.getAuditHistory();
+ }
+
+ @Override
+ public Result writeEntry(AuditEntry auditEntry) {
+ return auditor.writeEntry(auditEntry);
+ }
+
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java
new file mode 100644
index 000000000..a95f4e0d9
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditor.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.internal;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.ReplaceOptions;
+import com.mongodb.client.result.UpdateResult;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.common.core.audit.AuditReader;
+import io.flamingock.internal.common.mongodb.CollectionInitializator;
+import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.core.external.store.audit.LifecycleAuditWriter;
+import io.flamingock.internal.util.Result;
+import io.flamingock.internal.util.log.FlamingockLoggerFactory;
+import io.flamingock.reactive.util.PublisherSync;
+import org.bson.Document;
+import org.bson.conversions.Bson;
+import org.slf4j.Logger;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID;
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_EXECUTION_ID;
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_STATE;
+
+public class MongoDBReactiveAuditor implements LifecycleAuditWriter, AuditReader {
+
+ private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoDBReactiveAuditor");
+
+ private final MongoCollection collection;
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+
+ MongoDBReactiveAuditor(MongoDatabase database,
+ String collectionName,
+ ReadConcern readConcern,
+ ReadPreference readPreference,
+ WriteConcern writeConcern) {
+ this.collection = database.getCollection(collectionName)
+ .withReadConcern(readConcern)
+ .withReadPreference(readPreference)
+ .withWriteConcern(writeConcern);
+ }
+
+ protected void initialize(boolean autoCreate) {
+ CollectionInitializator initializer = new CollectionInitializator<>(
+ new MongoDBReactiveCollectionHelper(collection),
+ () -> new MongoDBDocumentHelper(new Document()),
+ new String[]{KEY_EXECUTION_ID, KEY_CHANGE_ID, KEY_STATE}
+ );
+ if (autoCreate) {
+ initializer.initialize();
+ } else {
+ initializer.justValidateCollection();
+ }
+
+ }
+
+ @Override
+ public Result writeEntry(AuditEntry auditEntry) {
+ Bson filter = Filters.and(
+ Filters.eq(KEY_EXECUTION_ID, auditEntry.getExecutionId()),
+ Filters.eq(KEY_CHANGE_ID, auditEntry.getChangeId()),
+ Filters.eq(KEY_STATE, auditEntry.getState().name())
+ );
+
+ Document entryDocument = mapper.toDocument(auditEntry).getDocument();
+
+ UpdateResult result = PublisherSync.first(
+ collection.replaceOne(filter, entryDocument, new ReplaceOptions().upsert(true)));
+ logger.debug("SaveOrUpdate[{}] with result" +
+ "\n[upsertId:{}, matches: {}, modifies: {}, acknowledged: {}]", auditEntry, result.getUpsertedId(), result.getMatchedCount(), result.getModifiedCount(), result.wasAcknowledged());
+
+ return Result.OK();
+ }
+
+
+ @Override
+ public List getAuditHistory() {
+ return PublisherSync.collect(collection.find())
+ .stream()
+ .map(MongoDBDocumentHelper::new)
+ .map(mapper::fromDocument)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java
new file mode 100644
index 000000000..a7e11257d
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockService.java
@@ -0,0 +1,180 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.internal;
+
+import com.mongodb.DuplicateKeyException;
+import com.mongodb.ErrorCategory;
+import com.mongodb.MongoWriteException;
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.result.UpdateResult;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.mongodb.CollectionInitializator;
+import io.flamingock.internal.common.mongodb.MongoDBLockMapper;
+import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.core.external.store.lock.LockAcquisition;
+import io.flamingock.internal.core.external.store.lock.LockKey;
+import io.flamingock.internal.core.external.store.lock.LockServiceException;
+import io.flamingock.internal.core.external.store.lock.community.CommunityLockEntry;
+import io.flamingock.internal.core.external.store.lock.community.CommunityLockService;
+import io.flamingock.internal.util.TimeService;
+import io.flamingock.internal.util.id.RunnerId;
+import io.flamingock.reactive.util.PublisherSync;
+import org.bson.Document;
+import org.bson.conversions.Bson;
+
+import java.util.Date;
+
+import static io.flamingock.internal.core.external.store.lock.LockStatus.LOCK_HELD;
+import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.EXPIRES_AT_FIELD;
+import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.KEY_FIELD;
+import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.OWNER_FIELD;
+import static io.flamingock.internal.core.external.store.lock.community.CommunityLockEntryConstants.STATUS_FIELD;
+
+public class MongoDBReactiveLockService implements CommunityLockService {
+
+ private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+
+
+ private final MongoCollection collection;
+ private final TimeService timeService;
+
+ public MongoDBReactiveLockService(MongoDatabase database,
+ String collectionName,
+ ReadConcern readConcern,
+ ReadPreference readPreference,
+ WriteConcern writeConcern,
+ TimeService timeService) {
+ this(database.getCollection(collectionName),
+ readConcern,
+ readPreference,
+ writeConcern,
+ timeService);
+ }
+
+ protected MongoDBReactiveLockService(MongoCollection collection,
+ ReadConcern readConcern,
+ ReadPreference readPreference,
+ WriteConcern writeConcern,
+ TimeService timeService) {
+ this.collection = collection
+ .withReadConcern(readConcern)
+ .withReadPreference(readPreference)
+ .withWriteConcern(writeConcern);
+ this.timeService = timeService;
+ }
+
+ public void initialize(boolean autoCreate) {
+ CollectionInitializator initializer = new CollectionInitializator<>(
+ new MongoDBReactiveCollectionHelper(collection),
+ () -> new MongoDBDocumentHelper(new Document()),
+ new String[]{KEY_FIELD}
+ );
+ if (autoCreate) {
+ initializer.initialize();
+ } else {
+ initializer.justValidateCollection();
+ }
+ }
+
+
+ @Override
+ public LockAcquisition upsert(LockKey key, RunnerId owner, long leaseMillis) {
+ CommunityLockEntry newLock = new CommunityLockEntry(key.toString(), LOCK_HELD, owner.toString(), timeService.currentDatePlusMillis(leaseMillis));
+ insertUpdate(newLock, false);
+ return new LockAcquisition(owner, leaseMillis);
+ }
+
+ @Override
+ public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis) throws LockServiceException {
+ CommunityLockEntry newLock = new CommunityLockEntry(key.toString(), LOCK_HELD, owner.toString(), timeService.currentDatePlusMillis(leaseMillis));
+ insertUpdate(newLock, true);
+ return new LockAcquisition(owner, leaseMillis);
+ }
+
+ @Override
+ public LockAcquisition getLockInfo(LockKey lockKey) {
+ Document result = PublisherSync.first(
+ collection.find(new Document().append(KEY_FIELD, lockKey.toString())).first());
+ if (result != null) {
+ return mapper.fromDocument(new MongoDBDocumentHelper(result));
+ }
+ return null;
+ }
+
+ @Override
+ public void releaseLock(LockKey lockKey, RunnerId owner) {
+ PublisherSync.first(collection.deleteMany(
+ Filters.and(Filters.eq(KEY_FIELD, lockKey.toString()), Filters.eq(OWNER_FIELD, owner.toString()))));
+ }
+
+ protected void insertUpdate(CommunityLockEntry newLock, boolean onlyIfSameOwner) {
+ boolean lockHeld;
+ String debErrorDetail = "not db error";
+ Bson acquireLockQuery = getAcquireLockQuery(newLock.getKey(), newLock.getOwner(), onlyIfSameOwner);
+ Document lockDocument = mapper.toDocument(newLock).getDocument();
+ Document newLockDocumentSet = new Document().append("$set", lockDocument);
+ try {
+ UpdateResult result = PublisherSync.first(collection.updateMany(
+ acquireLockQuery,
+ newLockDocumentSet,
+ new UpdateOptions().upsert(!onlyIfSameOwner)));
+ if (result == null) {
+ throw new LockServiceException(
+ acquireLockQuery.toString(),
+ newLockDocumentSet.toString(),
+ "MongoDB updateMany operation completed without returning an UpdateResult"
+ );
+ }
+ lockHeld = result.getModifiedCount() <= 0 && result.getUpsertedId() == null;
+
+ } catch (MongoWriteException ex) {
+ lockHeld = ex.getError().getCategory() == ErrorCategory.DUPLICATE_KEY;
+
+ if (!lockHeld) {
+ throw ex;
+ }
+ debErrorDetail = ex.getError().toString();
+
+ } catch (DuplicateKeyException ex) {
+ lockHeld = true;
+ debErrorDetail = ex.getMessage();
+ }
+
+ if (lockHeld) {
+ throw new LockServiceException(
+ acquireLockQuery.toString(),
+ newLockDocumentSet.toString(),
+ debErrorDetail
+ );
+ }
+ }
+
+ protected Bson getAcquireLockQuery(String lockKey, String owner, boolean onlyIfSameOwner) {
+ Bson expirationCond = Filters.lt(EXPIRES_AT_FIELD, new Date());
+ Bson ownerCond = Filters.eq(OWNER_FIELD, owner);
+ Bson keyCond = Filters.eq(KEY_FIELD, lockKey);
+ Bson statusCond = Filters.eq(STATUS_FIELD, LOCK_HELD.name());
+ return onlyIfSameOwner
+ ? Filters.and(keyCond, statusCond, ownerCond)
+ : Filters.and(keyCond, Filters.or(expirationCond, ownerCond));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore b/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore
new file mode 100644
index 000000000..199802661
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/main/resources/META-INF/services/io.flamingock.internal.core.external.store.CommunityAuditStore
@@ -0,0 +1 @@
+io.flamingock.store.mongodb.reactive.MongoDBReactiveAuditStore
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java
new file mode 100644
index 000000000..613eb3064
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditPersistenceE2ETest.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.common.test.pipeline.CodeChangeTestDefinition;
+import io.flamingock.store.mongodb.reactive.changes.audit._003__NonTxTargetSystemChange;
+import io.flamingock.store.mongodb.reactive.changes.audit._001__NonTxTransactionalFalseChange;
+import io.flamingock.store.mongodb.reactive.changes.audit._004__TxSeparateAndSameMongoClientChange;
+import io.flamingock.store.mongodb.reactive.changes.audit._005__TxSeparateChange;
+import io.flamingock.store.mongodb.reactive.changes.audit._002__TxSharedDefaultChange;
+import io.flamingock.core.kit.TestKit;
+import io.flamingock.core.kit.audit.AuditTestHelper;
+import io.flamingock.core.kit.audit.AuditTestSupport;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.common.core.audit.AuditTxType;
+import io.flamingock.targetsystem.nontransactional.NonTransactionalTargetSystem;
+import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit;
+import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+@Testcontainers
+class MongoDBReactiveAuditPersistenceE2ETest {
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4.6"));
+
+ private TestKit testKit;
+ private AuditTestHelper auditHelper;
+ private MongoClient sharedMongoClient;
+ private MongoClient separateMongoClient;
+ private MongoDatabase database;
+
+ @BeforeEach
+ void setUp() {
+ // Create shared MongoClient for audit storage and TX_SHARED scenarios
+ sharedMongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = sharedMongoClient.getDatabase("test");
+
+ // Create separate MongoClient for TX_SEPARATE_NO_MARKER scenarios
+ separateMongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+
+ // Initialize test kit with MongoDB persistence
+ testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test")), sharedMongoClient, database);
+ auditHelper = testKit.getAuditHelper();
+ }
+
+ @AfterEach
+ void tearDown() {
+ testKit.cleanUp();
+ }
+
+ @Test
+ @DisplayName("Should persist all audit entry fields correctly in MongoDB")
+ void testCompleteAuditEntryPersistenceInMongoDB() {
+ // Given
+ String changeId = "non-tx-transactional-false";
+ LocalDateTime testStart = LocalDateTime.now();
+ LocalDateTime testEnd = testStart.plusMinutes(5); // Allow enough time for test execution
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+
+ // Given-When-Then - Test MongoDB audit persistence with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED(changeId)
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_001__NonTxTransactionalFalseChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("mongodb")
+ .withSystemChange(false)
+ .withTimestampBetween(testStart, testEnd),
+ APPLIED(changeId)
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_001__NonTxTransactionalFalseChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("mongodb")
+ .withSystemChange(false)
+ .withTimestampBetween(testStart, testEnd)
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist NON_TX txStrategy for transactional=false scenarios")
+ void testNonTxScenarios() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+ // Given-When-Then - Test NON_TX scenarios with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_003__NonTxTargetSystemChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(new NonTransactionalTargetSystem("non-tx-system")) // Non-transactional target system
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ // First change (NonTxTransactionalFalseChange) - STARTED & EXECUTED
+ STARTED("non-tx-transactional-false")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_001__NonTxTransactionalFalseChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("mongodb"),
+ APPLIED("non-tx-transactional-false")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_001__NonTxTransactionalFalseChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("mongodb"),
+
+ // Second change (NonTxTargetSystemChange) - STARTED & EXECUTED
+ STARTED("non-tx-target-system")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_003__NonTxTargetSystemChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("non-tx-system"),
+ APPLIED("non-tx-target-system")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_003__NonTxTargetSystemChange.class)
+ .withTxType(AuditTxType.NON_TX)
+ .withTargetSystemId("non-tx-system")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist TX_SHARED txStrategy when targetSystem not defined in change")
+ void testTxSharedScenarios() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+ MongoDBReactiveTargetSystem sharedTargetSystem = new MongoDBReactiveTargetSystem("tx-shared-system", sharedMongoClient, "test"); // Same MongoClient as audit storage
+
+
+ // Given-When-Then - Test TX_SHARED scenarios with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(sharedTargetSystem)
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("tx-shared-default")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_002__TxSharedDefaultChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("mongodb"),
+ APPLIED("tx-shared-default")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_002__TxSharedDefaultChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("mongodb")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist TX_SEPARATE_NO_MARKER when targetSystem defined and different from auditStore")
+ void testTxNoMarkerWhenSameMongoClientButDifferentTargetSystem() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+ MongoDBReactiveTargetSystem sharedTargetSystem = new MongoDBReactiveTargetSystem("mongo-system", sharedMongoClient, "test"); // Same MongoClient as audit storage
+
+ // Given-When-Then - Test TX_SEPARATE_NO_MARKER scenarios with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_004__TxSeparateAndSameMongoClientChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(sharedTargetSystem)
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("tx-separate-no-marker")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_004__TxSeparateAndSameMongoClientChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("mongo-system"),
+ APPLIED("tx-separate-no-marker")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_004__TxSeparateAndSameMongoClientChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("mongo-system")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist TX_SEPARATE_NO_MARKER txStrategy for different MongoClient scenario")
+ void testTxSeparateNoMarkerScenario() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+ MongoDatabase separateDatabase = separateMongoClient.getDatabase("test");
+ MongoDBReactiveTargetSystem separateTargetSystem = new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test"); // Different MongoClient from audit storage
+
+ // Given-When-Then - Test TX_SEPARATE_NO_MARKER scenarios with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(separateTargetSystem)
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("tx-separate-no-marker")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_005__TxSeparateChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("tx-separate-system"),
+ APPLIED("tx-separate-no-marker")
+ .withType(AuditEntry.ChangeType.STANDARD_CODE)
+ .withClass(_005__TxSeparateChange.class)
+ .withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ .withTargetSystemId("tx-separate-system")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist correct targetSystemId for different target system configurations")
+ void testTargetSystemIdVariations() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+ MongoDatabase separateDatabase = separateMongoClient.getDatabase("test");
+
+ // Given-When-Then - Test multiple target system configurations with AuditTestSupport
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_003__NonTxTargetSystemChange.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList())
+ )
+ .WHEN(() -> {
+ assertDoesNotThrow(() -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(new NonTransactionalTargetSystem("non-tx-system"))
+ .addTargetSystem(new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test"))
+ .build()
+ .run();
+ });
+ })
+ .THEN_VerifyAuditSequenceStrict(
+ // TxSharedDefaultChange - STARTED & EXECUTED with mongodb target system
+ STARTED("tx-shared-default").withTargetSystemId("mongodb"),
+ APPLIED("tx-shared-default").withTargetSystemId("mongodb"),
+
+ // NonTxTargetSystemChange - STARTED & EXECUTED
+ STARTED("non-tx-target-system").withTargetSystemId("non-tx-system"),
+ APPLIED("non-tx-target-system").withTargetSystemId("non-tx-system"),
+
+ // TxSeparateChange - STARTED & EXECUTED with separate target system
+ STARTED("tx-separate-no-marker").withTargetSystemId("tx-separate-system"),
+ APPLIED("tx-separate-no-marker").withTargetSystemId("tx-separate-system")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should persist multiple changes with different txStrategy configurations correctly")
+ void testMultipleChangesWithDifferentConfigurations() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", sharedMongoClient, "test");
+
+
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__NonTxTransactionalFalseChange.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_002__TxSharedDefaultChange.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_005__TxSeparateChange.class, Collections.emptyList())
+ ).WHEN(() -> assertDoesNotThrow(() -> {
+ MongoDatabase separateDatabase = separateMongoClient.getDatabase("test");
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .addTargetSystem(new MongoDBReactiveTargetSystem("tx-separate-system", separateMongoClient, "test"))
+ .build()
+ .run();
+ }))
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("non-tx-transactional-false").withTxType(AuditTxType.NON_TX),
+ APPLIED("non-tx-transactional-false").withTxType(AuditTxType.NON_TX),
+
+ // TxSharedDefaultChange - STARTED & EXECUTED
+ STARTED("tx-shared-default").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER),
+ APPLIED("tx-shared-default").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER),
+
+ // TxSeparateChange - STARTED & EXECUTED
+ STARTED("tx-separate-no-marker").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER),
+ APPLIED("tx-separate-no-marker").withTxType(AuditTxType.TX_SEPARATE_NO_MARKER)
+ )
+ .run();
+
+
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java
new file mode 100644
index 000000000..dd052af4c
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveAuditStoreTest.java
@@ -0,0 +1,211 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.common.test.pipeline.CodeChangeTestDefinition;
+import io.flamingock.store.mongodb.reactive.changes._001__create_client_collection_happy;
+import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_non_transactional;
+import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_transactional;
+import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_failed_transactional_non_rollback;
+import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_happy_transactional;
+import io.flamingock.core.kit.TestKit;
+import io.flamingock.core.kit.audit.AuditTestHelper;
+import io.flamingock.core.kit.audit.AuditTestSupport;
+import io.flamingock.internal.core.operation.OperationException;
+import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit;
+import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.FAILED;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.ROLLED_BACK;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED;
+import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME;
+import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@Testcontainers
+class MongoDBReactiveAuditStoreTest {
+
+ private static final String DB_NAME = "test";
+
+ private static final String CLIENTS_COLLECTION = "clientCollection";
+
+ private static final String CUSTOM_AUDIT_REPOSITORY_NAME = "testFlamingockAudit";
+ private static final String CUSTOM_LOCK_REPOSITORY_NAME = "testFlamingockLock";
+
+ private static MongoClient mongoClient;
+
+ private static MongoDatabase database;
+
+ @Deprecated
+ private static MongoDBTestHelper mongoDBTestHelper;
+
+
+ @Container
+ public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"))
+ .withReuse(true);
+ private TestKit testKit;
+ private AuditTestHelper auditHelper;
+
+
+ @BeforeEach
+ void setupEach() {
+ mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = mongoClient.getDatabase("test");
+ testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test")), mongoClient, database);
+ auditHelper = testKit.getAuditHelper();
+
+ mongoDBTestHelper = new MongoDBTestHelper(database);
+
+ }
+
+ @AfterEach
+ void tearDown() {
+ ReactiveMongoTestHelper.complete(database.drop()); // Clean between tests
+ mongoClient.close();
+ }
+
+ @Test
+ @DisplayName("When standalone runs the AuditStore with DEFAULT repository names related collections should exists")
+ void happyPathWithDefaultRepositoryNames() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ //Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)),
+ new CodeChangeTestDefinition(_003__insert_jorge_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class))
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document"),
+ STARTED("insert-jorge-document"),
+ APPLIED("insert-jorge-document")
+ )
+ .run();
+
+ assertTrue(mongoDBTestHelper.collectionExists(DEFAULT_AUDIT_STORE_NAME));
+ assertTrue(mongoDBTestHelper.collectionExists(DEFAULT_LOCK_STORE_NAME));
+ assertFalse(mongoDBTestHelper.collectionExists(CUSTOM_AUDIT_REPOSITORY_NAME));
+ assertFalse(mongoDBTestHelper.collectionExists(CUSTOM_LOCK_REPOSITORY_NAME));
+ }
+
+ @Test
+ @DisplayName("When standalone runs the AuditStore with transactions enabled should persist the audit logs and the user's collection updated")
+ void happyPathWithTransaction() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ //Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class)),
+ new CodeChangeTestDefinition(_003__insert_jorge_happy_transactional.class, Arrays.asList(MongoDatabase.class, ClientSession.class))
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document"),
+ STARTED("insert-jorge-document"),
+ APPLIED("insert-jorge-document")
+ )
+ .run();
+
+ //Checking clients collection
+ Set clients = ReactiveMongoTestHelper.collect(database.getCollection(CLIENTS_COLLECTION)
+ .find())
+ .stream()
+ .map(document -> document.getString("name"))
+ .collect(Collectors.toCollection(HashSet::new));
+ assertEquals(2, clients.size());
+ assertTrue(clients.contains("Federico"));
+ assertTrue(clients.contains("Jorge"));
+ }
+
+ @Test
+ @DisplayName("When standalone runs the AuditStore with transactions enabled and execution fails should persist only the applied audit logs")
+ void failedWithTransaction() {
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ //Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.singletonList(MongoDatabase.class)),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.singletonList(MongoDatabase.class)),
+ new CodeChangeTestDefinition(_003__insert_jorge_failed_transactional_non_rollback.class, Arrays.asList(MongoDatabase.class, ClientSession.class))
+ )
+ .WHEN(() -> assertThrows(OperationException.class, () -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run();
+ }))
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document"),
+ STARTED("insert-jorge-document"),
+ FAILED("insert-jorge-document"),
+ ROLLED_BACK("insert-jorge-document")
+ )
+ .run();
+
+ //Checking clients collection
+ Set clients = ReactiveMongoTestHelper.collect(database.getCollection(CLIENTS_COLLECTION)
+ .find())
+ .stream()
+ .map(document -> document.getString("name"))
+ .collect(Collectors.toCollection(HashSet::new));
+ assertEquals(1, clients.size());
+ assertTrue(clients.contains("Federico"));
+ }
+
+
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java
new file mode 100644
index 000000000..53b263467
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBReactiveCoreStrategiesE2ETest.java
@@ -0,0 +1,245 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.common.test.pipeline.CodeChangeTestDefinition;
+import io.flamingock.targetsystem.mongodb.reactive.MongoDBReactiveTargetSystem;
+import io.flamingock.store.mongodb.reactive.changes._001__create_client_collection_happy;
+import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_non_transactional;
+import io.flamingock.store.mongodb.reactive.changes._002__insert_federico_happy_transactional;
+import io.flamingock.store.mongodb.reactive.changes._003__insert_jorge_failed_non_transactional_rollback;
+import io.flamingock.core.kit.audit.AuditTestSupport;
+import io.flamingock.mongodb.reactive.kit.MongoDBReactiveTestKit;
+import io.flamingock.core.kit.TestKit;
+import io.flamingock.core.kit.audit.AuditTestHelper;
+import io.flamingock.internal.core.operation.OperationException;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Collections;
+
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.APPLIED;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.FAILED;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.ROLLED_BACK;
+import static io.flamingock.core.kit.audit.AuditEntryExpectation.STARTED;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * MongoDB-specific version of CoreStrategiesE2ETest demonstrating unified TestKit API
+ * with real MongoDB storage using Testcontainers.
+ *
+ * This test suite replicates all scenarios from CoreStrategiesE2ETest but uses
+ * MongoDBReactiveTestKit with actual MongoDB persistence, proving the audit-store agnostic
+ * design works identically across storage implementations.
+ */
+@Testcontainers
+class MongoDBReactiveCoreStrategiesE2ETest {
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:4.4.6"))
+ .withReuse(true);
+
+ private MongoClient mongoClient;
+ private MongoDatabase database;
+
+ @BeforeAll
+ static void beforeAll() {
+ mongoDBContainer.start();
+ }
+
+ @BeforeEach
+ void setUp() {
+ mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = mongoClient.getDatabase("test");
+ }
+
+ @AfterEach
+ void tearDown() {
+ ReactiveMongoTestHelper.complete(database.drop()); // Clean between tests
+ mongoClient.close();
+ }
+
+ @Test
+ @DisplayName("Should execute non-transactional change using NonTx strategy with complete audit flow in MongoDB")
+ void testNonTransactionalChangeExecution() {
+ // Given - Create MongoDB test kit with real database storage
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+
+ // Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.emptyList())
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should execute transactional change using SimpleTx/SharedTx strategy with complete audit flow in MongoDB")
+ void testTransactionalChangeExecution() {
+ // Given - Create MongoDB test kit with real database storage
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+ AuditTestHelper auditHelper = testKit.getAuditHelper();
+
+ // Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_transactional.class, Collections.emptyList())
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should execute multiple changes with correct audit sequence in MongoDB")
+ void testMultipleChangesExecution() {
+ // Given - Create MongoDB test kit with real database storage
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+ AuditTestHelper auditHelper = testKit.getAuditHelper();
+
+ // Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_002__insert_federico_happy_non_transactional.class, Collections.emptyList())
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-federico-document"),
+ APPLIED("insert-federico-document")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should handle failing transactional change with proper audit and rollback in MongoDB")
+ void testFailingTransactionalChangeWithRollback() {
+ // Given - Create MongoDB test kit with real database storage
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+ AuditTestHelper auditHelper = testKit.getAuditHelper();
+
+ // Given-When-Then
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList()),
+ new CodeChangeTestDefinition(_003__insert_jorge_failed_non_transactional_rollback.class, Collections.emptyList())
+ )
+ .WHEN(() -> assertThrows(OperationException.class, () -> {
+ testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run();
+ }))
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection"),
+ STARTED("insert-jorge-document"),
+ FAILED("insert-jorge-document"),
+ ROLLED_BACK("insert-jorge-document")
+ )
+ .run();
+ }
+
+ @Test
+ @DisplayName("Should handle already-applied changes correctly on second run with MongoDB persistence")
+ void testAlreadyAppliedChangesSkipping() {
+ // Given - Create MongoDB test kit with persistent storage
+ MongoDBReactiveTargetSystem mongoDBSyncTargetSystem = new MongoDBReactiveTargetSystem("mongodb", mongoClient, "test");
+ TestKit testKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+ AuditTestHelper auditHelper = testKit.getAuditHelper();
+
+ // Given-When-Then - First execution
+ AuditTestSupport.withTestKit(testKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList())
+ )
+ .WHEN(() -> testKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection")
+ )
+ .run();
+
+ // Verify first execution with MongoDB storage
+ assertEquals(2, auditHelper.getAuditEntriesSorted().size());
+
+ // Second execution - create new kit using SAME MongoDB database to simulate persistence
+ TestKit secondRunKit = MongoDBReactiveTestKit.create(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem), mongoClient, database);
+
+ AuditTestSupport.withTestKit(secondRunKit)
+ .GIVEN_Changes(
+ new CodeChangeTestDefinition(_001__create_client_collection_happy.class, Collections.emptyList())
+ )
+ .WHEN(() -> secondRunKit.createBuilder()
+ .setAuditStore(MongoDBReactiveAuditStore.from(mongoDBSyncTargetSystem))
+ .addTargetSystem(mongoDBSyncTargetSystem)
+ .build()
+ .run())
+ .THEN_VerifyAuditSequenceStrict(
+ STARTED("create-client-collection"),
+ APPLIED("create-client-collection")
+ )
+ .run();
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java
new file mode 100644
index 000000000..646a6e563
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/MongoDBTestHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.util.TimeUtil;
+import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import org.bson.Document;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID;
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CREATED_AT;
+
+public class MongoDBTestHelper {
+ public final MongoDatabase mongoDatabase;
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+
+ public MongoDBTestHelper(MongoDatabase mongoDatabase) {
+ this.mongoDatabase = mongoDatabase;
+ }
+
+ public boolean collectionExists(String collectionName) {
+ return ReactiveMongoTestHelper.collect(mongoDatabase.listCollectionNames()).contains(collectionName);
+ }
+
+ public List getAuditLogSorted(String auditLogCollection) {
+ return ReactiveMongoTestHelper.collect(mongoDatabase.getCollection(auditLogCollection).find())
+ .stream()
+ .sorted(Comparator.comparing(d -> TimeUtil.toLocalDateTime(d.get(KEY_CREATED_AT))))
+ .map(document -> document.getString(KEY_CHANGE_ID))
+ .collect(Collectors.toList());
+ }
+
+ public List getAuditEntriesSorted(String auditLogCollection) {
+ return ReactiveMongoTestHelper.collect(mongoDatabase.getCollection(auditLogCollection).find())
+ .stream()
+ .map(MongoDBDocumentHelper::new)
+ .map(mapper::fromDocument)
+ .sorted()
+ .collect(Collectors.toList());
+ }
+
+
+
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java
new file mode 100644
index 000000000..a1a6dc0ae
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/ReactiveMongoTestHelper.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive;
+
+import io.flamingock.internal.common.core.error.FlamingockException;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class ReactiveMongoTestHelper {
+
+ private ReactiveMongoTestHelper() {
+ }
+
+ public static T first(Publisher publisher) {
+ List values = collect(publisher);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ public static List collect(Publisher publisher) {
+ List values = new ArrayList<>();
+ AtomicReference error = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ publisher.subscribe(new Subscriber() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T value) {
+ values.add(value);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error.set(throwable);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlamingockException(e);
+ }
+ Throwable throwable = error.get();
+ if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ }
+ if (throwable != null) {
+ throw new FlamingockException(throwable);
+ }
+ return values;
+ }
+
+ public static void complete(Publisher> publisher) {
+ collect(publisher);
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java
new file mode 100644
index 000000000..5c00cda9f
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_000__mongock_create_authors_collection.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Rollback;
+import io.mongock.api.annotations.ChangeUnit;
+import io.mongock.api.annotations.Execution;
+import io.mongock.api.annotations.RollbackExecution;
+import io.mongock.api.annotations.BeforeExecution;
+import io.mongock.api.annotations.RollbackBeforeExecution;
+import org.bson.Document;
+
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+
+@TargetSystem(id = "mongodb")
+@Change(id = "create-author-collection", author = "mongock")
+@ChangeUnit(id = "create-author-collection", order = "000", author = "mongock")
+public class _000__mongock_create_authors_collection {
+
+ public final static int INITIAL_CLIENTS = 10;
+ public final static String CLIENTS_COLLECTION_NAME = "mongockClientCollection";
+
+ @BeforeExecution
+ public void beforeExecution(MongoDatabase mongoDatabase) {
+
+ complete(mongoDatabase.createCollection(CLIENTS_COLLECTION_NAME));
+ }
+
+ @RollbackBeforeExecution
+ public void rollbackBeforeExecution(MongoDatabase mongoDatabase) {
+
+ complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME).drop());
+ }
+
+ @Apply
+ @Execution
+ public void apply(MongoDatabase mongoDatabase) {
+
+ complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME)
+ .insertMany(IntStream.range(0, INITIAL_CLIENTS)
+ .mapToObj(_000__mongock_create_authors_collection::getClient)
+ .collect(Collectors.toList())));
+ }
+
+ @Rollback
+ @RollbackExecution
+ public void rollback(MongoDatabase mongoDatabase) {
+ complete(mongoDatabase.getCollection(CLIENTS_COLLECTION_NAME).deleteMany(new Document()));
+ }
+
+ private static Document getClient(int i) {
+ return new Document()
+ .append("name", "name-" + i)
+ .append("email","email-" + i)
+ .append("phone","phone" + i).
+ append("country","country" + i);
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java
new file mode 100644
index 000000000..69d64af01
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_001__create_client_collection_happy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@Change( id="create-client-collection", transactional = false, author = "aperezdieppa")
+@TargetSystem(id = "mongodb")
+public class _001__create_client_collection_happy {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase) {
+ complete(mongoDatabase.createCollection("clientCollection"));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java
new file mode 100644
index 000000000..b3b5b76d5
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_non_transactional.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-federico-document", author = "aperezdieppa")
+public class _002__insert_federico_happy_non_transactional {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(new Document().append("name", "Federico")));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java
new file mode 100644
index 000000000..84c7c76d0
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_002__insert_federico_happy_transactional.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@Change( id="insert-federico-document" , author = "aperezdieppa")
+@TargetSystem(id = "mongodb")
+public class _002__insert_federico_happy_transactional {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(clientSession, new Document().append("name", "Federico")));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java
new file mode 100644
index 000000000..791d55d6a
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_non_rollback.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-jorge-document" , transactional = false, author = "aperezdieppa")
+public class _003__insert_jorge_failed_non_transactional_non_rollback {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(new Document().append("name", "Jorge")));
+ throw new RuntimeException("test");
+ }
+
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java
new file mode 100644
index 000000000..d0831f925
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_non_transactional_rollback.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import org.bson.Document;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import io.flamingock.api.annotations.Rollback;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-jorge-document" , author = "aperezdieppa")
+public class _003__insert_jorge_failed_non_transactional_rollback {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(new Document().append("name", "Jorge")));
+ throw new RuntimeException("test");
+ }
+
+ @Rollback
+ public void rollback(MongoDatabase mongoDatabase) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.deleteOne(new Document().append("name", "Jorge")));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java
new file mode 100644
index 000000000..2f19cc354
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_failed_transactional_non_rollback.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import org.bson.Document;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-jorge-document", author = "aperezdieppa")
+public class _003__insert_jorge_failed_transactional_non_rollback {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(clientSession, new Document().append("name", "Jorge")));
+ throw new RuntimeException("test");
+ }
+
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java
new file mode 100644
index 000000000..8791f0566
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_non_transactional.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-jorge-document" , author = "aperezdieppa")
+public class _003__insert_jorge_happy_non_transactional {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(new Document().append("name", "Jorge")));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java
new file mode 100644
index 000000000..893af04f0
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/_003__insert_jorge_happy_transactional.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.store.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem(id = "mongodb")
+@Change( id="insert-jorge-document", author = "aperezdieppa")
+public class _003__insert_jorge_happy_transactional {
+
+ @Apply
+ public void apply(MongoDatabase mongoDatabase, ClientSession clientSession) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(clientSession, new Document().append("name", "Jorge")));
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java
new file mode 100644
index 000000000..55deae024
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_001__NonTxTransactionalFalseChange.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+/**
+ * Change that produces NON_TX txStrategy via transactional=false annotation.
+ * Used for testing audit persistence of NON_TX transaction type.
+ */
+@TargetSystem(id = "mongodb")
+@Change(id = "non-tx-transactional-false", transactional = false, author = "aperezdieppa")
+public class _001__NonTxTransactionalFalseChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ System.out.println("Executing NON_TX change via transactional=false");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java
new file mode 100644
index 000000000..e0a1b0330
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_002__TxSharedDefaultChange.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+/**
+ * Change that produces TX_SHARED txStrategy via default behavior.
+ * Used for testing audit persistence of TX_SHARED transaction type.
+ */
+@TargetSystem(id = "mongodb")
+@Change(id = "tx-shared-default", transactional = true, author = "aperezdieppa")
+public class _002__TxSharedDefaultChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ System.out.println("Executing TX_SHARED change via default behavior");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java
new file mode 100644
index 000000000..514b88c28
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_003__NonTxTargetSystemChange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+
+/**
+ * Change that produces NON_TX txStrategy via non-transactional DefaultTargetSystem.
+ * Used for testing audit persistence of NON_TX transaction type with custom target system.
+ */
+@TargetSystem(id = "non-tx-system")
+@Change(id = "non-tx-target-system", author = "aperezdieppa")
+public class _003__NonTxTargetSystemChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ System.out.println("Executing NON_TX change via DefaultTargetSystem");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java
new file mode 100644
index 000000000..dd9246b13
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_004__TxSeparateAndSameMongoClientChange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+
+/**
+ * Change that produces TX_SHARED txStrategy via explicit MongoDBReactiveTargetSystem with same MongoClient.
+ * Used for testing audit persistence of TX_SHARED transaction type with explicit target system.
+ */
+@TargetSystem(id = "mongo-system")
+@Change(id = "tx-separate-no-marker", transactional = true, author = "aperezdieppa")
+public class _004__TxSeparateAndSameMongoClientChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ System.out.println("Executing TX_SHARED change via explicit MongoDBReactiveTargetSystem with same MongoClient");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java
new file mode 100644
index 000000000..666d3cb86
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_005__TxSeparateChange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+
+/**
+ * Change that produces TX_SEPARATE_NO_MARKER txStrategy via MongoDBReactiveTargetSystem with different MongoClient.
+ * Used for testing audit persistence of TX_SEPARATE_NO_MARKER transaction type.
+ */
+@TargetSystem(id = "tx-separate-system")
+@Change(id = "tx-separate-no-marker", transactional = true, author = "aperezdieppa")
+public class _005__TxSeparateChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ System.out.println("Executing TX_SEPARATE_NO_MARKER change via MongoDBReactiveTargetSystem with different MongoClient");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java
new file mode 100644
index 000000000..530fdf8ba
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_006__FailingChange.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+/**
+ * Change that intentionally fails to test error audit scenarios.
+ * Used for testing audit persistence of error fields like errorTrace.
+ */
+@TargetSystem(id = "mongodb")
+@Change(id = "failing-change", transactional = false, author = "aperezdieppa")
+public class _006__FailingChange {
+
+ @Apply
+ public void apply() {
+ // Intentionally throw exception to test error audit
+ throw new RuntimeException("Intentional failure for audit testing");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java
new file mode 100644
index 000000000..481cb6f58
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/changes/audit/_007__MetadataChange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.changes.audit;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.TargetSystem;
+/**
+ * Change for testing metadata serialization in audit entries.
+ * Used for testing audit persistence of metadata field.
+ */
+@TargetSystem(id = "mongodb")
+@Change(id = "metadata-change", transactional = false, author = "aperezdieppa")
+public class _007__MetadataChange {
+
+ @Apply
+ public void apply() {
+ // Simple operation that completes successfully
+ // Metadata testing will be done through test configuration
+ System.out.println("Executing change with metadata for audit testing");
+ }
+}
\ No newline at end of file
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java
new file mode 100644
index 000000000..da5dd7cd7
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveAuditPersistenceTest.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.internal;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.RecoveryStrategy;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.common.core.audit.AuditTxType;
+import io.flamingock.internal.core.configuration.community.CommunityConfigurable;
+import io.flamingock.internal.util.id.RunnerId;
+import io.flamingock.reactive.util.PublisherSync;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+
+@Testcontainers
+class MongoDBReactiveAuditPersistenceTest {
+
+ private static final String DB_NAME = "test";
+ private static final String AUDIT_COLLECTION = "testFlamingockAudit";
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ private MongoClient mongoClient;
+ private MongoDatabase database;
+ private MongoDBReactiveAuditPersistence persistence;
+
+ @BeforeEach
+ void beforeEach() {
+ mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = mongoClient.getDatabase(DB_NAME);
+ persistence = new MongoDBReactiveAuditPersistence(
+ mock(CommunityConfigurable.class),
+ database,
+ AUDIT_COLLECTION,
+ ReadConcern.MAJORITY,
+ ReadPreference.primary(),
+ WriteConcern.MAJORITY.withJournal(true),
+ true);
+ persistence.initialize(RunnerId.fromString("runner-1"));
+ }
+
+ @AfterEach
+ void afterEach() {
+ PublisherSync.complete(database.drop());
+ mongoClient.close();
+ }
+
+ @Test
+ @DisplayName("Should write and read audit entries")
+ void shouldWriteAndReadAuditEntries() {
+ AuditEntry started = auditEntry("change-1", AuditEntry.Status.STARTED);
+ AuditEntry applied = auditEntry("change-1", AuditEntry.Status.APPLIED);
+
+ persistence.writeEntry(started);
+ persistence.writeEntry(applied);
+
+ List history = persistence.getAuditHistory();
+ List states = history.stream()
+ .map(AuditEntry::getState)
+ .collect(Collectors.toList());
+
+ assertEquals(2, history.size());
+ assertTrue(states.contains(AuditEntry.Status.STARTED));
+ assertTrue(states.contains(AuditEntry.Status.APPLIED));
+ assertTrue(history.stream().allMatch(entry -> "change-1".equals(entry.getChangeId())));
+ }
+
+ @Test
+ @DisplayName("Should replace an existing audit entry with the same execution, change and state")
+ void shouldReplaceExistingAuditEntry() {
+ persistence.writeEntry(auditEntry("change-1", AuditEntry.Status.STARTED));
+ persistence.writeEntry(auditEntry("change-1", AuditEntry.Status.STARTED));
+
+ assertEquals(1, persistence.getAuditHistory().size());
+ }
+
+ private AuditEntry auditEntry(String changeId, AuditEntry.Status status) {
+ return new AuditEntry(
+ "execution-1",
+ "stage-1",
+ changeId,
+ "test-author",
+ LocalDateTime.now(),
+ status,
+ AuditEntry.ChangeType.STANDARD_CODE,
+ "TestChange",
+ "apply",
+ "TestChange.java",
+ 10L,
+ "localhost",
+ null,
+ false,
+ null,
+ AuditTxType.NON_TX,
+ "mongodb",
+ "001",
+ RecoveryStrategy.MANUAL_INTERVENTION,
+ false);
+ }
+}
diff --git a/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java
new file mode 100644
index 000000000..5aabcd4bd
--- /dev/null
+++ b/community/flamingock-mongodb-reactive-auditstore/src/test/java/io/flamingock/store/mongodb/reactive/internal/MongoDBReactiveLockServiceTest.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.store.mongodb.reactive.internal;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.core.external.store.lock.LockAcquisition;
+import io.flamingock.internal.core.external.store.lock.LockKey;
+import io.flamingock.internal.core.external.store.lock.LockServiceException;
+import io.flamingock.internal.util.TimeService;
+import io.flamingock.internal.util.id.RunnerId;
+import io.flamingock.reactive.util.PublisherSync;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@Testcontainers
+class MongoDBReactiveLockServiceTest {
+
+ private static final String DB_NAME = "test";
+ private static final String LOCK_COLLECTION = "testFlamingockLock";
+ private static final LockKey LOCK_KEY = LockKey.fromString("lockKey1");
+
+ @Container
+ static MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ private MongoClient mongoClient;
+ private MongoDatabase database;
+ private MongoDBReactiveLockService lockService;
+
+ @BeforeEach
+ void beforeEach() {
+ mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = mongoClient.getDatabase(DB_NAME);
+ lockService = new MongoDBReactiveLockService(
+ database,
+ LOCK_COLLECTION,
+ ReadConcern.MAJORITY,
+ ReadPreference.primary(),
+ WriteConcern.MAJORITY.withJournal(true),
+ TimeService.getDefault());
+ lockService.initialize(true);
+ }
+
+ @AfterEach
+ void afterEach() {
+ PublisherSync.complete(database.drop());
+ mongoClient.close();
+ }
+
+ @Test
+ @DisplayName("Should acquire and read lock")
+ void shouldAcquireAndReadLock() {
+ RunnerId runnerId = RunnerId.fromString("runner-1");
+
+ LockAcquisition lockAcquisition = lockService.upsert(LOCK_KEY, runnerId, 10000);
+ LockAcquisition lockInfo = lockService.getLockInfo(LOCK_KEY);
+
+ assertEquals(runnerId, lockAcquisition.getOwner());
+ assertEquals(10000, lockAcquisition.getAcquiredForMillis());
+ assertEquals(runnerId, lockInfo.getOwner());
+ }
+
+ @Test
+ @DisplayName("Should not acquire a held lock with a different owner")
+ void shouldNotAcquireHeldLockWithDifferentOwner() {
+ lockService.upsert(LOCK_KEY, RunnerId.fromString("runner-1"), 10000);
+
+ assertThrows(LockServiceException.class, () ->
+ lockService.upsert(LOCK_KEY, RunnerId.fromString("runner-2"), 10000));
+ }
+
+ @Test
+ @DisplayName("Should extend and release lock")
+ void shouldExtendAndReleaseLock() {
+ RunnerId runnerId = RunnerId.fromString("runner-1");
+ lockService.upsert(LOCK_KEY, runnerId, 10000);
+
+ LockAcquisition extension = lockService.extendLock(LOCK_KEY, runnerId, 20000);
+ lockService.releaseLock(LOCK_KEY, runnerId);
+
+ assertEquals(runnerId, extension.getOwner());
+ assertEquals(20000, extension.getAcquiredForMillis());
+ assertNull(lockService.getLockInfo(LOCK_KEY));
+ }
+}
diff --git a/community/flamingock-mongodb-sync-auditstore/build.gradle.kts b/community/flamingock-mongodb-sync-auditstore/build.gradle.kts
index ce322596c..df389916e 100644
--- a/community/flamingock-mongodb-sync-auditstore/build.gradle.kts
+++ b/community/flamingock-mongodb-sync-auditstore/build.gradle.kts
@@ -2,6 +2,7 @@ import org.jetbrains.kotlin.gradle.utils.extendsFrom
dependencies {
implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:mongodb-sync-util"))
implementation(project(":core:flamingock-core"))
api(project(":core:target-systems:flamingock-mongodb-externalsystem-api"))
@@ -34,4 +35,4 @@ java {
configurations.testImplementation {
extendsFrom(configurations.compileOnly.get())
-}
\ No newline at end of file
+}
diff --git a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java
index f9fa329e0..8d5e6a0cf 100644
--- a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java
+++ b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncAuditor.java
@@ -28,7 +28,7 @@
import io.flamingock.internal.common.mongodb.CollectionInitializator;
import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.core.external.store.audit.LifecycleAuditWriter;
import io.flamingock.internal.util.Result;
import io.flamingock.internal.util.log.FlamingockLoggerFactory;
@@ -49,7 +49,7 @@ public class MongoDBSyncAuditor implements LifecycleAuditWriter, AuditReader {
private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoDBSyncAuditor");
private final MongoCollection collection;
- private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document()));
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
MongoDBSyncAuditor(MongoDatabase database,
String collectionName,
@@ -63,9 +63,9 @@ public class MongoDBSyncAuditor implements LifecycleAuditWriter, AuditReader {
}
protected void initialize(boolean autoCreate) {
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(collection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{KEY_EXECUTION_ID, KEY_CHANGE_ID, KEY_STATE}
);
if (autoCreate) {
@@ -99,7 +99,7 @@ public List getAuditHistory() {
return collection.find()
.into(new LinkedList<>())
.stream()
- .map(MongoDBSyncDocumentHelper::new)
+ .map(MongoDBDocumentHelper::new)
.map(mapper::fromDocument)
.collect(Collectors.toList());
}
diff --git a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java
index fba382a3d..6387d8b44 100644
--- a/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java
+++ b/community/flamingock-mongodb-sync-auditstore/src/main/java/io/flamingock/store/mongodb/sync/internal/MongoDBSyncLockService.java
@@ -27,7 +27,7 @@
import com.mongodb.client.model.UpdateOptions;
import com.mongodb.client.result.UpdateResult;
import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.util.id.RunnerId;
import io.flamingock.internal.util.TimeService;
import io.flamingock.internal.core.external.store.lock.community.CommunityLockService;
@@ -50,7 +50,7 @@
public class MongoDBSyncLockService implements CommunityLockService {
- private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBSyncDocumentHelper(new Document()));
+ private final MongoDBLockMapper mapper = new MongoDBLockMapper<>(() -> new MongoDBDocumentHelper(new Document()));
private final MongoCollection collection;
@@ -82,9 +82,9 @@ protected MongoDBSyncLockService(MongoCollection collection,
}
public void initialize(boolean autoCreate) {
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(collection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{KEY_FIELD}
);
if (autoCreate) {
@@ -113,7 +113,7 @@ public LockAcquisition extendLock(LockKey key, RunnerId owner, long leaseMillis)
public LockAcquisition getLockInfo(LockKey lockKey) {
Document result = collection.find(new Document().append(KEY_FIELD, lockKey.toString())).first();
if (result != null) {
- return mapper.fromDocument(new MongoDBSyncDocumentHelper(result));
+ return mapper.fromDocument(new MongoDBDocumentHelper(result));
}
return null;
}
diff --git a/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java b/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java
index 90e89f2ae..1c1d02869 100644
--- a/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java
+++ b/community/flamingock-mongodb-sync-auditstore/src/test/java/io/flamingock/store/mongodb/sync/MongoDBTestHelper.java
@@ -17,7 +17,7 @@
import com.mongodb.client.MongoDatabase;
import io.flamingock.internal.common.core.audit.AuditEntry;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.util.TimeUtil;
import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
import org.bson.Document;
@@ -33,7 +33,7 @@
public class MongoDBTestHelper {
public final MongoDatabase mongoDatabase;
- private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document()));
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
public MongoDBTestHelper(MongoDatabase mongoDatabase) {
this.mongoDatabase = mongoDatabase;
@@ -57,7 +57,7 @@ public List getAuditEntriesSorted(String auditLogCollection) {
return mongoDatabase.getCollection(auditLogCollection).find()
.into(new LinkedList<>())
.stream()
- .map(MongoDBSyncDocumentHelper::new)
+ .map(MongoDBDocumentHelper::new)
.map(mapper::fromDocument)
.sorted()
.collect(Collectors.toList());
diff --git a/core/flamingock-bom/build.gradle.kts b/core/flamingock-bom/build.gradle.kts
index d3b8f9567..334a3551e 100644
--- a/core/flamingock-bom/build.gradle.kts
+++ b/core/flamingock-bom/build.gradle.kts
@@ -12,6 +12,7 @@ dependencies {
constraints {
// Flamingock Core
api("io.flamingock:flamingock-general-util:${generalUtilVersion}")
+ api("io.flamingock:flamingock-reactive-util:${version}")
api("io.flamingock:flamingock-core-api:${coreApiVersion}")
api("io.flamingock:flamingock-template-api:${templateApiVersion}")
api("io.flamingock:flamingock-test-support:${version}")
@@ -23,6 +24,7 @@ dependencies {
// Flamingock Community
api("io.flamingock:flamingock-community:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-auditstore:$version")
api("io.flamingock:flamingock-mongodb-sync-auditstore:$version")
api("io.flamingock:flamingock-mongodb-springdata-auditstore:${version}")
api("io.flamingock:flamingock-couchbase-auditstore:$version")
@@ -46,6 +48,9 @@ dependencies {
// Mongodb
api("io.flamingock:flamingock-mongodb-externalsystem-api:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-externalsystem-api:${version}")
+ api("io.flamingock:flamingock-mongodb-reactive-targetsystem:${version}")
+ api("io.flamingock:flamingock-mongodb-springdata-reactive-targetsystem:${version}")
api("io.flamingock:flamingock-mongodb-sync-targetsystem:${version}")
api("io.flamingock:flamingock-mongodb-springdata-targetsystem:${version}")
api("io.flamingock:flamingock-mongodb-sync-template:${mongodbTemplateVersion}")
diff --git a/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts
new file mode 100644
index 000000000..63012fb52
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/build.gradle.kts
@@ -0,0 +1,15 @@
+val coreApiVersion: String by extra
+
+dependencies {
+ implementation("io.flamingock:flamingock-core-api:${coreApiVersion}")
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0")
+}
+
+description = "MongoDB reactive external system api"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java
new file mode 100644
index 000000000..c2ba24770
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-externalsystem-api/src/main/java/io/flamingock/externalsystem/mongodb/reactive/api/MongoDBReactiveExternalSystem.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.externalsystem.mongodb.reactive.api;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.external.ExternalSystem;
+
+public interface MongoDBReactiveExternalSystem extends ExternalSystem {
+
+ MongoDatabase getMongoDatabase();
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts
new file mode 100644
index 000000000..ad3dbb813
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/build.gradle.kts
@@ -0,0 +1,26 @@
+dependencies {
+ api(project(":core:flamingock-core"))
+ implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:mongodb-reactive-util"))
+ api(project(":utils:flamingock-reactive-util"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api"))
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0")
+
+ testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2")
+ testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2")
+ testImplementation(project(":cloud:flamingock-cloud"))
+ testImplementation(project(":utils:test-util"))
+}
+
+description = "MongoDB reactive streams driver target system for database change operations"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java
new file mode 100644
index 000000000..f9daca643
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarker.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.client.model.Filters;
+import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.common.mongodb.CollectionInitializator;
+import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker;
+import io.flamingock.internal.core.transaction.TransactionManager;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import io.flamingock.reactive.util.PublisherSync;
+import org.bson.Document;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+
+public class MongoDBReactiveAuditMarker implements TargetSystemAuditMarker {
+ public static final String OPERATION = "operation";
+ private static final String CHANGE_ID = "changeId";
+ private final MongoCollection auditMarkerCollection;
+ private final TransactionManager txManager;
+
+ public MongoDBReactiveAuditMarker(MongoCollection auditMarkerCollection,
+ TransactionManager txManager) {
+ this.auditMarkerCollection = auditMarkerCollection;
+ this.txManager = txManager;
+ }
+
+ public static Builder builder(MongoDatabase mongoDatabase, TransactionManager txManager) {
+ return new Builder(mongoDatabase, txManager);
+ }
+
+ public static TargetSystemAuditMark mapToOnGoingStatus(Document document) {
+ TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString(OPERATION));
+ return new TargetSystemAuditMark(document.getString(CHANGE_ID), operation);
+ }
+
+ @Override
+ public Set listAll() {
+ return PublisherSync.collect(auditMarkerCollection.find())
+ .stream()
+ .map(MongoDBReactiveAuditMarker::mapToOnGoingStatus)
+ .collect(Collectors.toCollection(HashSet::new));
+ }
+
+ @Override
+ public void clearMark(String changeId) {
+ PublisherSync.first(auditMarkerCollection.deleteMany(Filters.eq(CHANGE_ID, changeId)));
+ }
+
+ @Override
+ public void mark(TargetSystemAuditMark auditMark) {
+
+ Document filter = new Document(CHANGE_ID, auditMark.getChangeId());
+
+ // Define the new document to replace or insert
+ Document newDocument = new Document(CHANGE_ID, auditMark.getChangeId())
+ .append(OPERATION, auditMark.getOperation().name());
+
+ ClientSession clientSession = txManager.getSessionOrThrow(auditMark.getChangeId());
+ PublisherSync.first(auditMarkerCollection.updateOne(
+ clientSession,
+ filter,
+ new Document("$set", newDocument),
+ new UpdateOptions().upsert(true)));
+ }
+
+ public static class Builder {
+
+ private final MongoDatabase mongoDatabase;
+ private final TransactionManager txManager;
+ private boolean autoCreate = true;
+ private String collectionName = CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME;
+ private ReadConcern readConcern;
+ private ReadPreference readPreference;
+ private WriteConcern writeConcern;
+
+ public Builder(MongoDatabase mongoDatabase, TransactionManager txManager) {
+ this.mongoDatabase = mongoDatabase;
+ this.txManager = txManager;
+ }
+
+ public Builder setCollectionName(String collectionName) {
+ this.collectionName = collectionName;
+ return this;
+ }
+
+ public Builder withAutoCreate(boolean autoCreate) {
+ this.autoCreate = autoCreate;
+ return this;
+ }
+
+ public Builder withReadConcern(ReadConcern readConcern) {
+ this.readConcern = readConcern;
+ return this;
+ }
+
+ public Builder withReadPreference(ReadPreference readPreference) {
+ this.readPreference = readPreference;
+ return this;
+ }
+
+ public Builder withWriteConcern(WriteConcern writeConcern) {
+ this.writeConcern = writeConcern;
+ return this;
+ }
+
+ public MongoDBReactiveAuditMarker build() {
+ MongoCollection collection = mongoDatabase.getCollection(collectionName)
+// .withReadConcern(readConcern)
+// .withReadPreference(readPreference)
+// .withWriteConcern(writeConcern)
+ ;
+
+ CollectionInitializator initializer = new CollectionInitializator<>(
+ new MongoDBReactiveCollectionHelper(collection),
+ () -> new MongoDBDocumentHelper(new Document()),
+ new String[]{CHANGE_ID}
+ );
+ if (autoCreate) {
+ initializer.initialize();
+ } else {
+ initializer.justValidateCollection();
+ }
+ return new MongoDBReactiveAuditMarker(collection, txManager);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java
new file mode 100644
index 000000000..6253a34fa
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystem.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem;
+import io.flamingock.internal.common.core.audit.AuditHistoryReader;
+import io.flamingock.internal.common.core.audit.AuditReaderType;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.common.core.error.FlamingockException;
+import io.flamingock.internal.common.core.transaction.TransactionWrapper;
+import io.flamingock.internal.core.builder.FlamingockEdition;
+import io.flamingock.internal.core.external.targets.TransactionalTargetSystem;
+import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker;
+import io.flamingock.internal.core.transaction.TransactionManager;
+import io.flamingock.reactive.util.PublisherSync;
+
+import java.util.Optional;
+
+import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY;
+
+public class MongoDBReactiveTargetSystem extends TransactionalTargetSystem
+ implements MongoDBReactiveExternalSystem {
+
+ private final MongoClient mongoClient;
+ private final String databaseName;
+ private MongoDatabase database;
+ private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true);
+ private ReadConcern readConcern = ReadConcern.MAJORITY;
+ private ReadPreference readPreference = ReadPreference.primary();
+ private MongoDBReactiveTxWrapper txWrapper;
+
+ public MongoDBReactiveTargetSystem(String id, MongoClient mongoClient, String databaseName) {
+ super(id);
+ this.mongoClient = mongoClient;
+ this.databaseName = databaseName;
+ }
+
+ public MongoDBReactiveTargetSystem withReadConcern(ReadConcern readConcern) {
+ this.readConcern = readConcern;
+ return this;
+ }
+
+ public MongoDBReactiveTargetSystem withReadPreference(ReadPreference readPreference) {
+ this.readPreference = readPreference;
+ return this;
+ }
+
+ public MongoDBReactiveTargetSystem withWriteConcern(WriteConcern writeConcern) {
+ this.writeConcern = writeConcern;
+ return this;
+ }
+
+ public MongoClient getClient() {
+ return mongoClient;
+ }
+
+ @Override
+ public MongoDatabase getMongoDatabase() {
+ if (mongoClient == null || databaseName == null || databaseName.isEmpty()) {
+ throw new FlamingockException("TargetSystem is not initialized. The 'mongoClient' instance and 'databaseName' property are required.");
+ }
+ return mongoClient.getDatabase(databaseName);
+ }
+
+ public String getDatabaseName() {
+ return databaseName;
+ }
+
+ public WriteConcern getWriteConcern() {
+ return writeConcern;
+ }
+
+ public ReadConcern getReadConcern() {
+ return readConcern;
+ }
+
+ public ReadPreference getReadPreference() {
+ return readPreference;
+ }
+
+ public TransactionManager getTxManager() {
+ return txWrapper.getTxManager();
+ }
+
+ @Override
+ public void initialize(ContextResolver baseContext) {
+ this.validate();
+ targetSystemContext.addDependency(mongoClient);
+ database = mongoClient.getDatabase(databaseName)
+ .withReadConcern(readConcern)
+ .withReadPreference(readPreference)
+ .withWriteConcern(writeConcern);
+ targetSystemContext.addDependency(database);
+
+ TransactionManager txManager =
+ new TransactionManager<>(() -> PublisherSync.first(mongoClient.startSession()));
+ txWrapper = new MongoDBReactiveTxWrapper(txManager);
+ FlamingockEdition edition = baseContext.getDependencyValue(FlamingockEdition.class).orElse(COMMUNITY);
+ auditMarker = edition == COMMUNITY
+ ? new NoOpTargetSystemAuditMarker(this.getId())
+ : MongoDBReactiveAuditMarker.builder(database, txManager).build();
+ }
+
+ private void validate() {
+ if (mongoClient == null) {
+ throw new FlamingockException("The 'mongoClient' instance is required.");
+ }
+ if (databaseName == null || databaseName.trim().isEmpty()) {
+ throw new FlamingockException("The 'databaseName' property is required.");
+ }
+ if (readConcern == null) {
+ throw new FlamingockException("The 'readConcern' property is required.");
+ }
+ if (readPreference == null) {
+ throw new FlamingockException("The 'readPreference' property is required.");
+ }
+ if (writeConcern == null) {
+ throw new FlamingockException("The 'writeConcern' property is required.");
+ }
+ }
+
+ @Override
+ protected MongoDBReactiveTargetSystem getSelf() {
+ return this;
+ }
+
+ @Override
+ public TransactionWrapper getTxWrapper() {
+ return txWrapper;
+ }
+
+ @Override
+ public Optional getAuditAuditReader(AuditReaderType type) {
+ return Optional.empty();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java
new file mode 100644
index 000000000..8b929d02a
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTxWrapper.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.TransactionOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+import io.flamingock.internal.common.core.context.Dependency;
+import io.flamingock.internal.common.core.context.RuntimeContext;
+import io.flamingock.internal.common.core.error.DatabaseTransactionException;
+import io.flamingock.internal.common.core.transaction.TransactionWrapper;
+import io.flamingock.internal.core.change.navigation.step.FailedStep;
+import io.flamingock.internal.core.transaction.TransactionManager;
+import io.flamingock.internal.util.log.FlamingockLoggerFactory;
+import io.flamingock.reactive.util.PublisherSync;
+import org.slf4j.Logger;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.function.Function;
+
+public class MongoDBReactiveTxWrapper implements TransactionWrapper {
+
+ private static final Logger logger = FlamingockLoggerFactory.getLogger("MongoReactiveTx");
+ private final TransactionManager sessionManager;
+
+ public MongoDBReactiveTxWrapper(TransactionManager sessionManager) {
+ this.sessionManager = sessionManager;
+ }
+
+ TransactionManager getTxManager() {
+ return sessionManager;
+ }
+
+ @Override
+ public RESULT wrapInTransaction(CONTEXT executionContext, Function operation) {
+ LocalDateTime transactionStart = LocalDateTime.now();
+ String sessionId = executionContext.getSessionId();
+ ClientSession clientSession = sessionManager.startSession(sessionId);
+ String connectionInfo = getConnectionInfo(clientSession);
+
+ try {
+ logger.debug("Starting MongoDB reactive transaction [connection={}]", connectionInfo);
+ clientSession.startTransaction(TransactionOptions.builder().build());
+ executionContext.addDependency(new Dependency(ClientSession.class, clientSession, false));
+
+ try {
+ RESULT result = operation.apply(executionContext);
+ Duration transactionDuration = Duration.between(transactionStart, LocalDateTime.now());
+ if (result instanceof FailedStep) {
+ logger.info("Rolling back MongoDB reactive transaction due to failed step [duration={}]", formatDuration(transactionDuration));
+ PublisherSync.complete(clientSession.abortTransaction());
+ logger.debug("MongoDB reactive transaction rollback completed successfully [duration={}]", formatDuration(transactionDuration));
+ } else {
+ logger.debug("Committing successful MongoDB reactive transaction [duration={}]", formatDuration(transactionDuration));
+ PublisherSync.complete(clientSession.commitTransaction());
+ logger.debug("MongoDB reactive transaction commit completed successfully [duration={}]", formatDuration(transactionDuration));
+ }
+ return result;
+ } catch (Exception e) {
+ Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now());
+ logger.debug("MongoDB reactive transaction failed, attempting rollback [duration={} error={}]",
+ formatDuration(failureDuration), e.getMessage());
+
+ DatabaseTransactionException.RollbackStatus rollbackStatus;
+ try {
+ PublisherSync.complete(clientSession.abortTransaction());
+ rollbackStatus = DatabaseTransactionException.RollbackStatus.SUCCESS;
+ logger.info("MongoDB reactive transaction rollback completed successfully after failure [duration={}]",
+ formatDuration(failureDuration));
+ } catch (Exception rollbackEx) {
+ rollbackStatus = DatabaseTransactionException.RollbackStatus.FAILED;
+ logger.debug("MongoDB reactive transaction rollback failed [duration={} rollback_error={}]",
+ formatDuration(failureDuration), rollbackEx.getMessage(), rollbackEx);
+ }
+ throw new DatabaseTransactionException(
+ "MongoDB reactive transaction failed during operation execution",
+ DatabaseTransactionException.TransactionState.FAILED,
+ null, // isolation level not applicable to MongoDB
+ null, // timeout not available
+ failureDuration,
+ rollbackStatus,
+ null, // specific operation not available at this level
+ connectionInfo,
+ e);
+ }
+ } finally {
+ try {
+ clientSession.close();
+ } finally {
+ sessionManager.closeSession(sessionId);
+ }
+ }
+ }
+
+ private String getConnectionInfo(ClientSession session) {
+ try {
+ return String.format("session_%s", session.getServerSession().getIdentifier());
+ } catch (Exception e) {
+ return "connection_info_unavailable";
+ }
+ }
+
+ private String formatDuration(Duration duration) {
+ long millis = duration.toMillis();
+ if (millis < 1000) {
+ return millis + "ms";
+ } else if (millis < 60000) {
+ return String.format("%.1fs", millis / 1000.0);
+ } else {
+ return String.format("%.1fm", millis / 60000.0);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java
new file mode 100644
index 000000000..51a317b97
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveAuditMarkerTest.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import io.flamingock.internal.core.transaction.TransactionManager;
+import io.flamingock.reactive.util.PublisherSync;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Testcontainers
+public class MongoDBReactiveAuditMarkerTest {
+
+ private static final String DB_NAME = "test";
+ private static final String MARKER_COLLECTION = "flamingockAuditMarkerTest";
+
+ @Container
+ public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ private static MongoClient mongoClient;
+ private static MongoDatabase database;
+
+ private TransactionManager txManager;
+ private MongoDBReactiveAuditMarker marker;
+
+ @BeforeAll
+ static void beforeAll() {
+ mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ database = mongoClient.getDatabase(DB_NAME);
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ txManager = new TransactionManager<>(() -> PublisherSync.first(mongoClient.startSession()));
+ marker = MongoDBReactiveAuditMarker.builder(database, txManager)
+ .setCollectionName(MARKER_COLLECTION)
+ .withAutoCreate(true)
+ .build();
+ clearAll();
+ }
+
+ @AfterEach
+ void afterEach() {
+ clearAll();
+ }
+
+ @Test
+ @DisplayName("mark() persists each mark and listAll() returns them with the right contents")
+ void markPersistsAndIsReadableViaListAll() {
+ String changeId1 = "change-1";
+ String changeId2 = "change-2";
+
+ startSession(changeId1);
+ startSession(changeId2);
+ marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED));
+ marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.ROLLED_BACK));
+
+ Set marks = marker.listAll();
+ Assertions.assertEquals(2, marks.size());
+
+ Map byId = marks.stream()
+ .collect(Collectors.toMap(TargetSystemAuditMark::getChangeId,
+ TargetSystemAuditMark::getOperation));
+ Assertions.assertEquals(TargetSystemAuditMarkType.APPLIED, byId.get(changeId1));
+ Assertions.assertEquals(TargetSystemAuditMarkType.ROLLED_BACK, byId.get(changeId2));
+ }
+
+ @Test
+ @DisplayName("clearMark() removes only the targeted mark")
+ void clearMarkRemovesOnlyTheTargetedMark() {
+ String changeId1 = "change-1";
+ String changeId2 = "change-2";
+
+ startSession(changeId1);
+ startSession(changeId2);
+ marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED));
+ marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.APPLIED));
+
+ marker.clearMark(changeId1);
+
+ Set marks = marker.listAll();
+ Assertions.assertEquals(1, marks.size());
+ Assertions.assertEquals(changeId2, marks.iterator().next().getChangeId());
+ }
+
+ private void startSession(String changeId) {
+ txManager.startSession(changeId);
+ }
+
+ private void clearAll() {
+ PublisherSync.complete(database.getCollection(MARKER_COLLECTION).drop());
+ marker = MongoDBReactiveAuditMarker.builder(database, txManager)
+ .setCollectionName(MARKER_COLLECTION)
+ .withAutoCreate(true)
+ .build();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java
new file mode 100644
index 000000000..539af5d1f
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemE2ETest.java
@@ -0,0 +1,295 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.external.TargetSystem;
+import io.flamingock.targetsystem.mongodb.reactive.changes.happypath._001__HappyCreateClientsCollectionChange;
+import io.flamingock.targetsystem.mongodb.reactive.changes.happypath._002__HappyInsertClientsChange;
+import io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath._001__UnhappyCreateClientsCollectionChange;
+import io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath._002__UnhappyInsertClientsChange;
+import io.flamingock.common.test.cloud.AuditRequestExpectation;
+import io.flamingock.common.test.cloud.MockRunnerServer;
+import io.flamingock.common.test.cloud.execution.ExecutionContinueRequestResponseMock;
+import io.flamingock.common.test.cloud.execution.ExecutionPlanRequestResponseMock;
+import io.flamingock.common.test.cloud.mock.MockRequestResponseChange;
+import io.flamingock.common.test.cloud.prototype.PrototypeClientSubmission;
+import io.flamingock.common.test.cloud.prototype.PrototypeStage;
+import io.flamingock.internal.util.Trio;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.builder.FlamingockFactory;
+import io.flamingock.internal.core.builder.CloudChangeRunnerBuilder;
+import io.flamingock.internal.common.core.metadata.MetadataLoader;
+import io.flamingock.internal.core.operation.OperationException;
+import io.flamingock.internal.core.builder.runner.Runner;
+import org.junit.jupiter.api.*;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Collections;
+
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.APPLIED;
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.FAILED;
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.ROLLED_BACK;
+
+@Testcontainers
+public class MongoDBReactiveTargetSystemE2ETest {
+
+ private static final Logger logger = LoggerFactory.getLogger(MongoDBReactiveTargetSystemE2ETest.class);
+
+ private static final String DB_NAME = "test";
+ private static final String CLIENTS_COLLECTION = "clientCollection";
+
+ private static MongoClient mongoClient;
+ private static MongoDatabase testDatabase;
+ private static MongoDBTestHelper mongoDBTestHelper;
+
+ private final String apiToken = "FAKE_API_TOKEN";
+ private final long organisationId = 1L;
+ private final String organisationName = "MyOrganisation";
+ private final long projectId = 2L;
+ private final String projectName = "MyOrganisation";
+ private final String serviceName = "clients-service";
+ private final String environmentName = "development";
+ private final long serviceId = 3L;
+ private final long environmentId = 4L;
+ private final long credentialId = 5L;
+ private final int runnerServerPort = 8888;
+ private final String jwt = "fake_jwt";
+
+ private MockRunnerServer mockRunnerServer;
+ private CloudChangeRunnerBuilder flamingockBuilder;
+
+ @Container
+ public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ @BeforeAll
+ static void beforeAll() {
+ mongoClient = MongoClients.create(MongoClientSettings
+ .builder()
+ .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString()))
+ .build());
+ testDatabase = mongoClient.getDatabase(DB_NAME);
+ mongoDBTestHelper = new MongoDBTestHelper(testDatabase);
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ mockRunnerServer = new MockRunnerServer()
+ .setServerPort(runnerServerPort)
+ .setOrganisationId(organisationId)
+ .setOrganisationName(organisationName)
+ .setProjectId(projectId)
+ .setProjectName(projectName)
+ .setServiceId(serviceId)
+ .setServiceName(serviceName)
+ .setEnvironmentId(environmentId)
+ .setEnvironmentName(environmentName)
+ .setCredentialId(credentialId)
+ .setApiToken(apiToken)
+ .setJwt(jwt);
+
+ flamingockBuilder = FlamingockFactory.getCloudBuilder()
+ .setApiToken(apiToken)
+ .setHost("http://localhost:" + runnerServerPort)
+ .setService(serviceName)
+ .setEnvironment(environmentName);
+ }
+
+ @AfterEach
+ void afterEach() throws Exception {
+ //tear down
+ mockRunnerServer.stop();
+
+ ReactiveMongoTestHelper.complete(testDatabase.getCollection(CLIENTS_COLLECTION).drop());
+ ReactiveMongoTestHelper.complete(testDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME).drop());
+ }
+
+ @Test
+ @DisplayName("Should follow the targetSystem lifecycle")
+ void happyPath() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)) {
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", APPLIED)
+ ).start();
+
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)),
+ new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class))
+ ));
+
+
+ TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME);
+
+ flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build()
+ .execute();
+
+ //THEN
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1);
+ // check ongoing status
+ mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0);
+ }
+ }
+
+ @Test
+ @DisplayName("Should rollback the ongoing deletion when a change fails")
+ void failedChanges() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__UnhappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__UnhappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (
+ MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)
+ ) {
+ MongoDBReactiveTargetSystem mongoTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME);
+
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", FAILED),
+ new AuditRequestExpectation(executionId, "insert-clients", ROLLED_BACK)
+ ).start();
+
+ MongoDBReactiveTargetSystem mongoSyncCloudTransactioner = Mockito.spy(mongoTargetSystem);
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__UnhappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)),
+ new Trio<>(_002__UnhappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class))
+ ));
+
+ TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME);
+
+ Runner runner = flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build();
+
+ //THEN
+ OperationException ex = Assertions.assertThrows(OperationException.class, runner::run);
+
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 0);
+
+ // check ongoing status
+ mongoDBTestHelper.checkEmptyTargetSystemAudiMarker();
+ }
+ }
+
+
+ //TODO verify the server is called with the right parameters. among other, it sends the ongoing status
+ @Test
+ @Disabled("adapt when adding cloud support")
+ @DisplayName("Should send ongoing change in execution when is present in local database")
+ void shouldSendOngoingChangeInExecutionPlan() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (
+ MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)
+
+ ) {
+ MongoDBReactiveTargetSystem mongoTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME);
+
+ mongoDBTestHelper.insertOngoingExecution("insert-clients");
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId, new MockRequestResponseChange("insert-clients", TargetSystemAuditMarkType.APPLIED)),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", APPLIED)
+ ).start();
+
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(MongoDatabase.class)),
+ new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(MongoDatabase.class))
+ ));
+ TargetSystem mongoDBTargetSystem = new MongoDBReactiveTargetSystem("mongodb-ts", mongoClient, DB_NAME);
+
+ flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build()
+ .execute();
+
+ //THEN
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1);
+ // check ongoing status
+ mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java
new file mode 100644
index 000000000..8605f03eb
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBReactiveTargetSystemTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.core.builder.FlamingockEdition;
+import org.junit.jupiter.api.Test;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MongoDBReactiveTargetSystemTest {
+
+ @Test
+ void initializesReactiveDependencies() {
+ MongoClient client = mock(MongoClient.class);
+ MongoDatabase database = mock(MongoDatabase.class);
+ when(client.getDatabase("test")).thenReturn(database);
+ when(database.withReadConcern(ReadConcern.MAJORITY)).thenReturn(database);
+ when(database.withReadPreference(ReadPreference.primary())).thenReturn(database);
+ when(database.withWriteConcern(WriteConcern.MAJORITY.withJournal(true))).thenReturn(database);
+
+ ContextResolver baseContext = mock(ContextResolver.class);
+ when(baseContext.getDependencyValue(FlamingockEdition.class))
+ .thenReturn(Optional.of(FlamingockEdition.COMMUNITY));
+
+ MongoDBReactiveTargetSystem targetSystem =
+ new MongoDBReactiveTargetSystem("mongodb", client, "test");
+ targetSystem.initialize(baseContext);
+
+ assertSame(client, targetSystem.getContext().getRequiredDependencyValue(MongoClient.class));
+ assertSame(database, targetSystem.getContext().getRequiredDependencyValue(MongoDatabase.class));
+ assertSame(database, targetSystem.getMongoDatabase());
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java
new file mode 100644
index 000000000..283964028
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/MongoDBTestHelper.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle;
+import io.flamingock.internal.common.mongodb.CollectionInitializator;
+import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import org.bson.Document;
+
+import java.util.HashSet;
+import java.util.function.Predicate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MongoDBTestHelper {
+
+ public final MongoDatabase mongoDatabase;
+
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+
+ public MongoDBTestHelper(MongoDatabase mongoDatabase) {
+ this.mongoDatabase = mongoDatabase;
+ }
+
+ public void insertOngoingExecution(String changeId) {
+
+ MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME);
+
+ CollectionInitializator initializer = new CollectionInitializator<>(
+ new MongoDBReactiveCollectionHelper(onGoingChangesCollection),
+ () -> new MongoDBDocumentHelper(new Document()),
+ new String[]{"changeId"}
+ );
+ initializer.initialize();
+
+
+ Document filter = new Document("changeId", changeId);
+
+ Document newDocument = new Document("changeId", changeId)
+ .append("operation", AuditContextBundle.Operation.EXECUTION.toString());
+
+ ReactiveMongoTestHelper.complete(onGoingChangesCollection.updateOne(
+ filter,
+ new Document("$set", newDocument),
+ new com.mongodb.client.model.UpdateOptions().upsert(true)));
+
+ checkEmptyTargetSystemAudiMarker();
+ }
+
+ public void checkCount(MongoCollection collection, int count) {
+ long result = ReactiveMongoTestHelper.collect(collection
+ .find())
+ .stream()
+ .count();
+ assertEquals(count, (int) result);
+ }
+
+ public void checkEmptyTargetSystemAudiMarker() {
+ checkOngoingChange(result -> result == 0);
+ }
+
+ public void checkOngoingChange(Predicate predicate) {
+ MongoCollection onGoingChangesCollection = mongoDatabase.getCollection("flamingockOnGoingChanges");
+
+ long result = ReactiveMongoTestHelper.collect(onGoingChangesCollection.find())
+ .stream()
+ .map(MongoDBTestHelper::mapToOnGoingStatus)
+ .collect(java.util.stream.Collectors.toCollection(HashSet::new))
+ .size();
+
+ assertTrue(predicate.test(result));
+ }
+
+ public static TargetSystemAuditMark mapToOnGoingStatus(Document document) {
+ TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString("operation"));
+ return new TargetSystemAuditMark(document.getString("changeId"), operation);
+ }
+
+
+
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java
new file mode 100644
index 000000000..ad61beee9
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/PipelineTestHelper.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import io.flamingock.api.StageType;
+import io.flamingock.api.annotations.TargetSystem;
+import io.flamingock.internal.common.core.metadata.FlamingockMetadata;
+import io.flamingock.internal.common.core.preview.PreviewConstructor;
+import io.flamingock.internal.common.core.change.RecoveryDescriptor;
+import io.flamingock.internal.common.core.change.TargetSystemDescriptor;
+import io.flamingock.internal.core.change.loaded.ChangeOrderUtil;
+import io.flamingock.internal.util.Pair;
+import io.flamingock.internal.util.Trio;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.internal.common.core.preview.CodePreviewChange;
+import io.flamingock.internal.common.core.preview.PreviewMethod;
+import io.flamingock.internal.common.core.preview.PreviewPipeline;
+import io.flamingock.internal.common.core.preview.PreviewStage;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class PipelineTestHelper {
+
+
+ private static final Function, ChangeInfo> infoExtractor = c -> {
+ Change ann = c.getAnnotation(Change.class);
+ TargetSystem targetSystemAnn = c.getAnnotation(TargetSystem.class);
+ String targetSystemId = targetSystemAnn != null ? targetSystemAnn.id() : null;
+ String changeId = ann.id();
+ String order = ChangeOrderUtil.getMatchedOrderFromClassName(changeId, null, c.getName());
+ return new ChangeInfo(changeId, order, ann.author(), targetSystemId, ann.transactional());
+ };
+
+ @NotNull
+ private static List getParameterTypes(List> second) {
+ return second
+ .stream()
+ .map(Class::getName)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Builds a {@link PreviewPipeline} composed of a single {@link PreviewStage} containing one or more {@link CodePreviewChange}s.
+ *
+ * Each change is derived from a {@link Pair} where:
+ *
+ * - The first item is the {@link Class} annotated with {@link Change}
+ * - The second item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Apply}
+ * - The third item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Rollback}
+ *
+ *
+ * @param changeDefinitions varargs of pairs containing change classes and their execution method parameters
+ * @return a {@link PreviewPipeline} ready for preview or testing
+ */
+ @SafeVarargs
+ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio, List>, List>>... changeDefinitions) {
+
+ List changes = Arrays.stream(changeDefinitions)
+ .map(trio -> {
+ Function, ChangeInfo> extractor = infoExtractor;
+ ChangeInfo changeInfo = extractor.apply(trio.getFirst());
+ PreviewMethod rollback = null;
+ if (trio.getThird() != null) {
+ rollback = new PreviewMethod("rollback", getParameterTypes(trio.getThird()));
+ }
+
+ List changeList = new ArrayList<>();
+ changeList.add(new CodePreviewChange(
+ changeInfo.getChangeId(),
+ changeInfo.getOrder(),
+ changeInfo.getAuthor(),
+ trio.getFirst().getName(),
+ null,
+ PreviewConstructor.getDefault(),
+ new PreviewMethod("apply", getParameterTypes(trio.getSecond())),
+ rollback,
+ false,
+ changeInfo.transactional,
+ false,
+ changeInfo.targetSystem,
+ RecoveryDescriptor.getDefault(),
+ false
+ ));
+ return changeList;
+ })
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+
+ PreviewStage stage = new PreviewStage(
+ stageName,
+ StageType.DEFAULT,
+ "some description",
+ null,
+ null,
+ changes
+ );
+
+ PreviewPipeline previewPipeline = new PreviewPipeline(Collections.singletonList(stage));
+ return new FlamingockMetadata(previewPipeline, null, null);
+ }
+
+ @SafeVarargs
+ public static FlamingockMetadata getPreviewPipeline(Trio, List>, List>>... changeDefinitions) {
+ return getPreviewPipeline("default-stage-name", changeDefinitions);
+ }
+
+ static class ChangeInfo {
+ private final String changeId;
+ private final String order;
+ private final String author;
+ private final TargetSystemDescriptor targetSystem;
+ private final boolean transactional;
+
+ public ChangeInfo(String changeId, String order, String author,String targetSystemId, boolean transactional) {
+ this.changeId = changeId;
+ this.order = order;
+ this.author = author;
+ this.targetSystem = new TargetSystemDescriptor(targetSystemId);
+ this.transactional = transactional;
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+
+ public String getOrder() {
+ return order;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public TargetSystemDescriptor getTargetSystem() {
+ return targetSystem;
+ }
+
+ public boolean isTransactional() {
+ return transactional;
+ }
+ }
+
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java
new file mode 100644
index 000000000..76b485f45
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/ReactiveMongoTestHelper.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive;
+
+import io.flamingock.internal.common.core.error.FlamingockException;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class ReactiveMongoTestHelper {
+
+ private ReactiveMongoTestHelper() {
+ }
+
+ public static T first(Publisher publisher) {
+ List values = collect(publisher);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ public static List collect(Publisher publisher) {
+ List values = new ArrayList<>();
+ AtomicReference error = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ publisher.subscribe(new Subscriber() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T value) {
+ values.add(value);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error.set(throwable);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlamingockException(e);
+ }
+ Throwable throwable = error.get();
+ if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ }
+ if (throwable != null) {
+ throw new FlamingockException(throwable);
+ }
+ return values;
+ }
+
+ public static void complete(Publisher> publisher) {
+ collect(publisher);
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java
new file mode 100644
index 000000000..8878ee413
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive.changes.happypath;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+
+import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false)
+public class _001__HappyCreateClientsCollectionChange {
+
+ @Apply
+ public void apply(@NonLockGuarded MongoDatabase mongoDatabase) {
+ complete(mongoDatabase.createCollection("clientCollection"));
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java
new file mode 100644
index 000000000..1b609a601
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/happypath/_002__HappyInsertClientsChange.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive.changes.happypath;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "insert-clients", author = "aperezdieppa")
+public class _002__HappyInsertClientsChange {
+
+ @Apply
+ public void apply(@NonLockGuarded MongoDatabase mongoDatabase, @NonLockGuarded ClientSession clientSession) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(clientSession, new Document().append("Name", "Federico")));
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java
new file mode 100644
index 000000000..898716def
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+
+import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false)
+public class _001__UnhappyCreateClientsCollectionChange {
+
+ @Apply
+ public void apply(@NonLockGuarded MongoDatabase mongoDatabase) {
+ complete(mongoDatabase.createCollection("clientCollection"));
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java
new file mode 100644
index 000000000..012bcb8b6
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.reactive.changes.unhappypath;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+
+import static io.flamingock.targetsystem.mongodb.reactive.ReactiveMongoTestHelper.complete;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "insert-clients", author = "aperezdieppa")
+public class _002__UnhappyInsertClientsChange {
+
+ @Apply
+ public void apply(@NonLockGuarded MongoDatabase mongoDatabase, @NonLockGuarded ClientSession clientSession) {
+ MongoCollection collection = mongoDatabase.getCollection("clientCollection");
+ complete(collection.insertOne(clientSession, new Document().append("name", "Should Have Been Rolled Back")));
+ throw new RuntimeException("Intended exception");
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts
new file mode 100644
index 000000000..298cde8af
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/build.gradle.kts
@@ -0,0 +1,35 @@
+import org.jetbrains.kotlin.gradle.utils.extendsFrom
+
+val versions = mapOf(
+ "mongodb" to "4.0.0",
+ "spring-data" to "3.1.4"
+)
+
+dependencies {
+ api(project(":core:flamingock-core"))
+ api(project(":utils:flamingock-reactive-util"))
+ api(project(":core:target-systems:flamingock-mongodb-reactive-externalsystem-api"))
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:${versions["mongodb"]}")
+ compileOnly("org.springframework.data:spring-data-mongodb:${versions["spring-data"]}")
+ compileOnly("io.projectreactor:reactor-core:3.4.34")
+
+ testImplementation("org.testcontainers:testcontainers-mongodb:2.0.2")
+ testImplementation("org.testcontainers:testcontainers-junit-jupiter:2.0.2")
+ testImplementation(project(":cloud:flamingock-cloud"))
+ testImplementation(project(":utils:test-util"))
+ testImplementation(project(":utils:mongodb-util"))
+ testImplementation(project(":utils:mongodb-reactive-util"))
+}
+
+description = "MongoDB Spring Data reactive target system for Spring-integrated applications"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java
new file mode 100644
index 000000000..6435d9716
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarker.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import org.bson.Document;
+import org.springframework.data.domain.Sort;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.index.Index;
+import org.springframework.data.mongodb.core.index.IndexInfo;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.springframework.data.mongodb.core.query.Criteria.where;
+
+/**
+ * Audit marker for the MongoDB Spring Data reactive target system.
+ *
+ * Writes participate in the active Spring-managed MongoDB transaction by going through
+ * {@link ReactiveMongoTemplate}. This keeps the Spring reactive abstraction intact and
+ * avoids exposing the underlying reactive {@code ClientSession} through the Flamingock
+ * {@code TransactionManager}.
+ */
+public class MongoDBSpringDataReactiveAuditMarker implements TargetSystemAuditMarker {
+
+ public static final String OPERATION = "operation";
+ private static final String CHANGE_ID = "changeId";
+
+ private final ReactiveMongoTemplate mongoTemplate;
+ private final String collectionName;
+
+ public MongoDBSpringDataReactiveAuditMarker(ReactiveMongoTemplate mongoTemplate, String collectionName) {
+ this.mongoTemplate = mongoTemplate;
+ this.collectionName = collectionName;
+ }
+
+ public static Builder builder(ReactiveMongoTemplate mongoTemplate) {
+ return new Builder(mongoTemplate);
+ }
+
+ public static TargetSystemAuditMark mapToOnGoingStatus(Document document) {
+ TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString(OPERATION));
+ return new TargetSystemAuditMark(document.getString(CHANGE_ID), operation);
+ }
+
+ @Override
+ public Set listAll() {
+ return mongoTemplate.findAll(Document.class, collectionName)
+ .map(MongoDBSpringDataReactiveAuditMarker::mapToOnGoingStatus)
+ .collectList()
+ .block()
+ .stream()
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public void clearMark(String changeId) {
+ mongoTemplate.remove(Query.query(where(CHANGE_ID).is(changeId)), collectionName).block();
+ }
+
+ @Override
+ public void mark(TargetSystemAuditMark auditMark) {
+ Query query = Query.query(where(CHANGE_ID).is(auditMark.getChangeId()));
+ Update update = new Update()
+ .set(CHANGE_ID, auditMark.getChangeId())
+ .set(OPERATION, auditMark.getOperation().name());
+ mongoTemplate.upsert(query, update, collectionName).block();
+ }
+
+ public static class Builder {
+
+ private final ReactiveMongoTemplate mongoTemplate;
+ private boolean autoCreate = true;
+ private String collectionName = CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME;
+
+ public Builder(ReactiveMongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ }
+
+ public Builder setCollectionName(String collectionName) {
+ this.collectionName = collectionName;
+ return this;
+ }
+
+ public Builder withAutoCreate(boolean autoCreate) {
+ this.autoCreate = autoCreate;
+ return this;
+ }
+
+ public MongoDBSpringDataReactiveAuditMarker build() {
+ List indexes = mongoTemplate.indexOps(collectionName).getIndexInfo().collectList().block();
+ boolean validIndex = indexes.stream()
+ .anyMatch(index -> index.isUnique() && index.isIndexForFields(java.util.Collections.singleton(CHANGE_ID)));
+ if (!validIndex && !autoCreate) {
+ throw new IllegalStateException(
+ "Index creation not allowed, but not created or wrongly created for collection " + collectionName);
+ }
+ if (!validIndex) {
+ mongoTemplate.indexOps(collectionName)
+ .ensureIndex(new Index().on(CHANGE_ID, Sort.Direction.ASC).unique())
+ .block();
+ }
+ return new MongoDBSpringDataReactiveAuditMarker(mongoTemplate, collectionName);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java
new file mode 100644
index 000000000..39e42c720
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystem.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.externalsystem.mongodb.reactive.api.MongoDBReactiveExternalSystem;
+import io.flamingock.internal.common.core.audit.AuditHistoryReader;
+import io.flamingock.internal.common.core.audit.AuditReaderType;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.common.core.error.FlamingockException;
+import io.flamingock.internal.common.core.transaction.TransactionWrapper;
+import io.flamingock.internal.core.builder.FlamingockEdition;
+import io.flamingock.internal.core.external.targets.TransactionalTargetSystem;
+import io.flamingock.internal.core.external.targets.mark.NoOpTargetSystemAuditMarker;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+import java.util.Optional;
+
+import static io.flamingock.internal.core.builder.FlamingockEdition.COMMUNITY;
+
+public class MongoDBSpringDataReactiveTargetSystem
+ extends TransactionalTargetSystem
+ implements MongoDBReactiveExternalSystem {
+
+ private final ReactiveMongoTemplate mongoTemplate;
+ private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true);
+ private ReadConcern readConcern = ReadConcern.MAJORITY;
+ private ReadPreference readPreference = ReadPreference.primary();
+
+ private MongoDBSpringDataReactiveTxWrapper txWrapper;
+
+ public MongoDBSpringDataReactiveTargetSystem(String id, ReactiveMongoTemplate mongoTemplate) {
+ super(id);
+ this.mongoTemplate = mongoTemplate;
+ }
+
+ public MongoDBSpringDataReactiveTargetSystem withReadConcern(ReadConcern readConcern) {
+ this.readConcern = readConcern;
+ return this;
+ }
+
+ public MongoDBSpringDataReactiveTargetSystem withReadPreference(ReadPreference readPreference) {
+ this.readPreference = readPreference;
+ return this;
+ }
+
+ public MongoDBSpringDataReactiveTargetSystem withWriteConcern(WriteConcern writeConcern) {
+ this.writeConcern = writeConcern;
+ return this;
+ }
+
+ public ReactiveMongoTemplate getMongoTemplate() {
+ return mongoTemplate;
+ }
+
+ public WriteConcern getWriteConcern() {
+ return writeConcern;
+ }
+
+ public ReadConcern getReadConcern() {
+ return readConcern;
+ }
+
+ public ReadPreference getReadPreference() {
+ return readPreference;
+ }
+
+ @Override
+ public MongoDatabase getMongoDatabase() {
+ if (mongoTemplate == null) {
+ throw new FlamingockException("TargetSystem is not initialized. The 'mongoTemplate' instance is required.");
+ }
+ return mongoTemplate.getMongoDatabaseFactory().getMongoDatabase().block();
+ }
+
+ @Override
+ public void initialize(ContextResolver baseContext) {
+ this.validate();
+ targetSystemContext.addDependency(mongoTemplate);
+
+ txWrapper = MongoDBSpringDataReactiveTxWrapper.builder()
+ .mongoTemplate(mongoTemplate)
+ .readConcern(readConcern)
+ .readPreference(readPreference)
+ .writeConcern(writeConcern)
+ .build();
+
+ FlamingockEdition edition = baseContext.getDependencyValue(FlamingockEdition.class).orElse(COMMUNITY);
+ auditMarker = edition == COMMUNITY
+ ? new NoOpTargetSystemAuditMarker(this.getId())
+ : MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate).build();
+ }
+
+ private void validate() {
+ if (mongoTemplate == null) {
+ throw new FlamingockException("The 'mongoTemplate' instance is required.");
+ }
+ if (readConcern == null) {
+ throw new FlamingockException("The 'readConcern' property is required.");
+ }
+ if (readPreference == null) {
+ throw new FlamingockException("The 'readPreference' property is required.");
+ }
+ if (writeConcern == null) {
+ throw new FlamingockException("The 'writeConcern' property is required.");
+ }
+ }
+
+ @Override
+ protected MongoDBSpringDataReactiveTargetSystem getSelf() {
+ return this;
+ }
+
+ @Override
+ public TransactionWrapper getTxWrapper() {
+ return txWrapper;
+ }
+
+ @Override
+ public Optional getAuditAuditReader(AuditReaderType type) {
+ return Optional.empty();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java
new file mode 100644
index 000000000..c97bdc59e
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTxWrapper.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadPreference;
+import com.mongodb.TransactionOptions;
+import com.mongodb.WriteConcern;
+import com.mongodb.ClientSessionOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+import io.flamingock.internal.common.core.context.Dependency;
+import io.flamingock.internal.common.core.context.RuntimeContext;
+import io.flamingock.internal.common.core.error.DatabaseTransactionException;
+import io.flamingock.internal.common.core.transaction.TransactionWrapper;
+import io.flamingock.internal.core.change.navigation.step.FailedStep;
+import io.flamingock.internal.util.log.FlamingockLoggerFactory;
+import org.slf4j.Logger;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.util.function.Function;
+
+public class MongoDBSpringDataReactiveTxWrapper implements TransactionWrapper {
+
+ private static final Logger logger = FlamingockLoggerFactory.getLogger("SpringMongoReactiveTx");
+ private final ReactiveMongoTemplate mongoTemplate;
+ private final TransactionOptions txOptions;
+
+ private MongoDBSpringDataReactiveTxWrapper(ReactiveMongoTemplate mongoTemplate, TransactionOptions txOptions) {
+ this.mongoTemplate = mongoTemplate;
+ this.txOptions = txOptions;
+ }
+
+ @Override
+ public RESULT wrapInTransaction(CONTEXT executionContext, Function operation) {
+ LocalDateTime transactionStart = LocalDateTime.now();
+ ClientSession clientSession = null;
+ try {
+ logger.debug("Starting MongoDB Spring Data reactive transaction");
+
+ clientSession = mongoTemplate.getMongoDatabaseFactory()
+ .getSession(ClientSessionOptions.builder().build())
+ .block();
+ clientSession.startTransaction(txOptions);
+
+ ReactiveMongoTemplate sessionMongoTemplate = new ReactiveMongoTemplate(
+ mongoTemplate.getMongoDatabaseFactory().withSession(clientSession),
+ mongoTemplate.getConverter());
+ executionContext.addDependency(new Dependency(ReactiveMongoTemplate.class, sessionMongoTemplate));
+
+ try {
+ RESULT result = operation.apply(executionContext);
+ Duration transactionDuration = Duration.between(transactionStart, LocalDateTime.now());
+
+ if (result instanceof FailedStep) {
+ logger.info("Rolling back MongoDB Spring Data reactive transaction due to failed step [duration={}]", formatDuration(transactionDuration));
+ abortTransaction(clientSession);
+ logger.debug("MongoDB Spring Data reactive transaction rollback completed successfully [duration={}]", formatDuration(transactionDuration));
+ } else {
+ logger.debug("Committing successful MongoDB Spring Data reactive transaction [duration={}]", formatDuration(transactionDuration));
+ commitTransaction(clientSession);
+ logger.debug("MongoDB Spring Data reactive transaction commit completed successfully [duration={}]", formatDuration(transactionDuration));
+ }
+ return result;
+
+ } catch (Exception e) {
+ Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now());
+ logger.debug("MongoDB Spring Data reactive transaction failed, attempting rollback [duration={} error={}]",
+ formatDuration(failureDuration), e.getMessage());
+ abortTransaction(clientSession);
+
+ throw new DatabaseTransactionException(
+ "MongoDB Spring Data reactive transaction failed during operation execution",
+ DatabaseTransactionException.TransactionState.FAILED,
+ null, // isolation level not applicable to MongoDB
+ null, // timeout not available
+ failureDuration,
+ DatabaseTransactionException.RollbackStatus.SUCCESS, // Spring handles rollback
+ null, // specific operation not available at this level
+ "Spring Data Reactive MongoDB",
+ e);
+ }
+ } catch (Exception e) {
+ Duration failureDuration = Duration.between(transactionStart, LocalDateTime.now());
+ if (e instanceof DatabaseTransactionException) {
+ throw e;
+ }
+ throw new DatabaseTransactionException(
+ "MongoDB Spring Data reactive transaction failed to start or commit",
+ DatabaseTransactionException.TransactionState.FAILED,
+ null, // isolation level not applicable to MongoDB
+ null, // timeout not available
+ failureDuration,
+ DatabaseTransactionException.RollbackStatus.SUCCESS, // Spring handles rollback
+ null, // specific operation not available at this level
+ "Spring Data Reactive MongoDB",
+ e);
+ } finally {
+ executionContext.addDependency(new Dependency(ReactiveMongoTemplate.class, mongoTemplate));
+ if (clientSession != null) {
+ clientSession.close();
+ }
+ }
+ }
+
+ private void commitTransaction(ClientSession clientSession) {
+ reactor.core.publisher.Mono.from(clientSession.commitTransaction()).block();
+ }
+
+ private void abortTransaction(ClientSession clientSession) {
+ reactor.core.publisher.Mono.from(clientSession.abortTransaction()).block();
+ }
+
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public static class Builder {
+
+ private ReadConcern readConcern = ReadConcern.MAJORITY;
+ private ReadPreference readPreference = ReadPreference.primary();
+ private WriteConcern writeConcern = WriteConcern.MAJORITY.withJournal(true);
+ private ReactiveMongoTemplate mongoTemplate;
+
+ private Builder() {
+ }
+
+ public Builder readConcern(ReadConcern readConcern) {
+ this.readConcern = readConcern;
+ return this;
+ }
+
+ public Builder readPreference(ReadPreference readPreference) {
+ this.readPreference = readPreference;
+ return this;
+ }
+
+ public Builder writeConcern(WriteConcern writeConcern) {
+ this.writeConcern = writeConcern;
+ return this;
+ }
+
+ public Builder mongoTemplate(ReactiveMongoTemplate mongoTemplate) {
+ this.mongoTemplate = mongoTemplate;
+ return this;
+ }
+
+ public MongoDBSpringDataReactiveTxWrapper build() {
+ if (mongoTemplate == null) {
+ throw new IllegalArgumentException("ReactiveMongoTemplate is required");
+ }
+ TransactionOptions txOptions = TransactionOptions.builder()
+ .readConcern(readConcern)
+ .readPreference(readPreference)
+ .writeConcern(writeConcern)
+ .build();
+ return new MongoDBSpringDataReactiveTxWrapper(mongoTemplate, txOptions);
+ }
+ }
+
+ private String formatDuration(Duration duration) {
+ long millis = duration.toMillis();
+ if (millis < 1000) {
+ return millis + "ms";
+ } else if (millis < 60000) {
+ return String.format("%.1fs", millis / 1000.0);
+ } else {
+ return String.format("%.1fm", millis / 60000.0);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java
new file mode 100644
index 000000000..b766d261a
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveAuditMarkerTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@Testcontainers
+public class MongoDBSpringDataReactiveAuditMarkerTest {
+
+ private static final String DB_NAME = "test";
+ private static final String MARKER_COLLECTION = "flamingockAuditMarkerTest";
+
+ @Container
+ public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ private static ReactiveMongoTemplate mongoTemplate;
+
+ private MongoDBSpringDataReactiveAuditMarker marker;
+
+ @BeforeAll
+ static void beforeAll() {
+ MongoClient mongoClient = MongoClients.create(mongoDBContainer.getConnectionString());
+ mongoTemplate = new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(mongoClient, DB_NAME));
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ marker = MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate)
+ .setCollectionName(MARKER_COLLECTION)
+ .withAutoCreate(true)
+ .build();
+ clearAll();
+ }
+
+ @AfterEach
+ void afterEach() {
+ clearAll();
+ }
+
+ @Test
+ @DisplayName("mark() persists each mark and listAll() returns them with the right contents")
+ void markPersistsAndIsReadableViaListAll() {
+ String changeId1 = "change-1";
+ String changeId2 = "change-2";
+
+ marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED));
+ marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.ROLLED_BACK));
+
+ Set marks = marker.listAll();
+ Assertions.assertEquals(2, marks.size());
+
+ Map byId = marks.stream()
+ .collect(Collectors.toMap(TargetSystemAuditMark::getChangeId,
+ TargetSystemAuditMark::getOperation));
+ Assertions.assertEquals(TargetSystemAuditMarkType.APPLIED, byId.get(changeId1));
+ Assertions.assertEquals(TargetSystemAuditMarkType.ROLLED_BACK, byId.get(changeId2));
+ }
+
+ @Test
+ @DisplayName("clearMark() removes only the targeted mark")
+ void clearMarkRemovesOnlyTheTargetedMark() {
+ String changeId1 = "change-1";
+ String changeId2 = "change-2";
+
+ marker.mark(new TargetSystemAuditMark(changeId1, TargetSystemAuditMarkType.APPLIED));
+ marker.mark(new TargetSystemAuditMark(changeId2, TargetSystemAuditMarkType.APPLIED));
+
+ marker.clearMark(changeId1);
+
+ Set marks = marker.listAll();
+ Assertions.assertEquals(1, marks.size());
+ Assertions.assertEquals(changeId2, marks.iterator().next().getChangeId());
+ }
+
+ private void clearAll() {
+ mongoTemplate.dropCollection(MARKER_COLLECTION).block();
+ marker = MongoDBSpringDataReactiveAuditMarker.builder(mongoTemplate)
+ .setCollectionName(MARKER_COLLECTION)
+ .withAutoCreate(true)
+ .build();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java
new file mode 100644
index 000000000..dcae06ef6
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemE2ETest.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.ConnectionString;
+import com.mongodb.MongoClientSettings;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.api.external.TargetSystem;
+import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath._001__HappyCreateClientsCollectionChange;
+import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath._002__HappyInsertClientsChange;
+import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath._001__UnhappyCreateClientsCollectionChange;
+import io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath._002__UnhappyInsertClientsChange;
+import io.flamingock.common.test.cloud.AuditRequestExpectation;
+import io.flamingock.common.test.cloud.MockRunnerServer;
+import io.flamingock.common.test.cloud.execution.ExecutionContinueRequestResponseMock;
+import io.flamingock.common.test.cloud.execution.ExecutionPlanRequestResponseMock;
+import io.flamingock.common.test.cloud.mock.MockRequestResponseChange;
+import io.flamingock.common.test.cloud.prototype.PrototypeClientSubmission;
+import io.flamingock.common.test.cloud.prototype.PrototypeStage;
+import io.flamingock.internal.util.Trio;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.builder.FlamingockFactory;
+import io.flamingock.internal.core.builder.CloudChangeRunnerBuilder;
+import io.flamingock.internal.common.core.metadata.MetadataLoader;
+import io.flamingock.internal.core.operation.OperationException;
+import io.flamingock.internal.core.builder.runner.Runner;
+import org.junit.jupiter.api.*;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
+import org.testcontainers.containers.MongoDBContainer;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.Collections;
+
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.APPLIED;
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.FAILED;
+import static io.flamingock.cloud.api.vo.CloudAuditStatus.ROLLED_BACK;
+
+@Testcontainers
+public class MongoDBSpringDataReactiveTargetSystemE2ETest {
+
+ private static final Logger logger = LoggerFactory.getLogger(MongoDBSpringDataReactiveTargetSystemE2ETest.class);
+
+ private static final String DB_NAME = "test";
+ private static final String CLIENTS_COLLECTION = "clientCollection";
+
+ private static MongoDatabase testDatabase;
+ private static ReactiveMongoTemplate mongoTemplate;
+ private static MongoDBTestHelper mongoDBTestHelper;
+
+ private final String apiToken = "FAKE_API_TOKEN";
+ private final long organisationId = 1L;
+ private final String organisationName = "MyOrganisation";
+ private final long projectId = 2L;
+ private final String projectName = "MyOrganisation";
+ private final String serviceName = "clients-service";
+ private final String environmentName = "development";
+ private final long serviceId = 3L;
+ private final long environmentId = 4L;
+ private final long credentialId = 5L;
+ private final int runnerServerPort = 8888;
+ private final String jwt = "fake_jwt";
+
+ private MockRunnerServer mockRunnerServer;
+ private CloudChangeRunnerBuilder flamingockBuilder;
+
+ @Container
+ public static final MongoDBContainer mongoDBContainer = new MongoDBContainer(DockerImageName.parse("mongo:6"));
+
+ @BeforeAll
+ static void beforeAll() {
+ MongoClient mongoClient = MongoClients.create(MongoClientSettings
+ .builder()
+ .applyConnectionString(new ConnectionString(mongoDBContainer.getConnectionString()))
+ .build());
+ testDatabase = mongoClient.getDatabase(DB_NAME);
+ mongoTemplate = new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(mongoClient, DB_NAME));
+ mongoDBTestHelper = new MongoDBTestHelper(testDatabase);
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ mockRunnerServer = new MockRunnerServer()
+ .setServerPort(runnerServerPort)
+ .setOrganisationId(organisationId)
+ .setOrganisationName(organisationName)
+ .setProjectId(projectId)
+ .setProjectName(projectName)
+ .setServiceId(serviceId)
+ .setServiceName(serviceName)
+ .setEnvironmentId(environmentId)
+ .setEnvironmentName(environmentName)
+ .setCredentialId(credentialId)
+ .setApiToken(apiToken)
+ .setJwt(jwt);
+
+ flamingockBuilder = FlamingockFactory.getCloudBuilder()
+ .setApiToken(apiToken)
+ .setHost("http://localhost:" + runnerServerPort)
+ .setService(serviceName)
+ .setEnvironment(environmentName);
+ }
+
+ @AfterEach
+ void afterEach() throws Exception {
+ //tear down
+ mockRunnerServer.stop();
+
+ ReactiveMongoTestHelper.complete(testDatabase.getCollection(CLIENTS_COLLECTION).drop());
+ ReactiveMongoTestHelper.complete(testDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME).drop());
+ }
+
+ @Test
+ @DisplayName("Should follow the targetSystem lifecycle")
+ void happyPath() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)) {
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", APPLIED)
+ ).start();
+
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)),
+ new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class))
+ ));
+
+
+ TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate);
+
+ flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build()
+ .execute();
+
+ //THEN
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1);
+ // check ongoing status
+ mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0);
+ }
+ }
+
+ @Test
+ @DisplayName("Should rollback the ongoing deletion when a change fails")
+ void failedChanges() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__UnhappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__UnhappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (
+ MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)
+ ) {
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", FAILED),
+ new AuditRequestExpectation(executionId, "insert-clients", ROLLED_BACK)
+ ).start();
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__UnhappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)),
+ new Trio<>(_002__UnhappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class))
+ ));
+
+ TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate);
+
+ Runner runner = flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build();
+
+ //THEN
+ OperationException ex = Assertions.assertThrows(OperationException.class, runner::run);
+
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 0);
+
+ // check ongoing status
+ mongoDBTestHelper.checkEmptyTargetSystemAudiMarker();
+ }
+ }
+
+
+ //TODO verify the server is called with the right parameters. among other, it sends the ongoing status
+ @Test
+ @Disabled("adapt when adding cloud support")
+ @DisplayName("Should send ongoing change in execution when is present in local database")
+ void shouldSendOngoingChangeInExecutionPlan() {
+ long executionId = 1L;
+ String stageName = "stage-1";
+
+ PrototypeClientSubmission prototypeClientSubmission = new PrototypeClientSubmission(
+ new PrototypeStage(stageName, 0)
+ .addChange("create-clients-collection", _001__HappyCreateClientsCollectionChange.class.getName(), "apply", false)
+ .addChange("insert-clients", _002__HappyInsertClientsChange.class.getName(), "apply", true)
+ );
+
+ //GIVEN
+ try (
+ MockedStatic mocked = Mockito.mockStatic(MetadataLoader.class)
+
+ ) {
+ MongoDBSpringDataReactiveTargetSystem mongoTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate);
+
+ mongoDBTestHelper.insertOngoingExecution("insert-clients");
+ mockRunnerServer
+ .withClientSubmissionBase(prototypeClientSubmission)
+ .withExecutionPlanRequestsExpectation(
+ new ExecutionPlanRequestResponseMock(executionId, new MockRequestResponseChange("insert-clients", TargetSystemAuditMarkType.APPLIED)),
+ new ExecutionContinueRequestResponseMock()
+ ).withAuditRequestsExpectation(
+ new AuditRequestExpectation(executionId, "create-clients-collection", APPLIED),
+ new AuditRequestExpectation(executionId, "insert-clients", APPLIED)
+ ).start();
+
+
+ //WHEN
+ mocked.when(MetadataLoader::loadAggregated).thenReturn(PipelineTestHelper.getPreviewPipeline(
+ "stage-1",
+ new Trio<>(_001__HappyCreateClientsCollectionChange.class, Collections.singletonList(ReactiveMongoTemplate.class)),
+ new Trio<>(_002__HappyInsertClientsChange.class, Collections.singletonList(ReactiveMongoTemplate.class))
+ ));
+ TargetSystem mongoDBTargetSystem = new MongoDBSpringDataReactiveTargetSystem("mongodb-ts", mongoTemplate);
+
+ flamingockBuilder
+ .addTargetSystem(mongoDBTargetSystem)
+ .build()
+ .execute();
+
+ //THEN
+ mockRunnerServer.verifyAllCalls();
+
+ // check clients changes
+ mongoDBTestHelper.checkCount(testDatabase.getCollection(CLIENTS_COLLECTION), 1);
+ // check ongoing status
+ mongoDBTestHelper.checkOngoingChange(ongoingCount -> ongoingCount == 0);
+ }
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java
new file mode 100644
index 000000000..849727da1
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBSpringDataReactiveTargetSystemTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.context.ContextResolver;
+import io.flamingock.internal.core.builder.FlamingockEdition;
+import org.junit.jupiter.api.Test;
+import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import reactor.core.publisher.Mono;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MongoDBSpringDataReactiveTargetSystemTest {
+
+ @Test
+ void initializesReactiveMongoTemplateDependency() {
+ ReactiveMongoTemplate template = mock(ReactiveMongoTemplate.class);
+ ReactiveMongoDatabaseFactory databaseFactory = mock(ReactiveMongoDatabaseFactory.class);
+ MongoDatabase database = mock(MongoDatabase.class);
+ when(template.getMongoDatabaseFactory()).thenReturn(databaseFactory);
+ when(databaseFactory.getMongoDatabase()).thenReturn(Mono.just(database));
+
+ ContextResolver baseContext = mock(ContextResolver.class);
+ when(baseContext.getDependencyValue(FlamingockEdition.class))
+ .thenReturn(Optional.of(FlamingockEdition.COMMUNITY));
+
+ MongoDBSpringDataReactiveTargetSystem targetSystem =
+ new MongoDBSpringDataReactiveTargetSystem("mongodb", template);
+ targetSystem.initialize(baseContext);
+
+ assertSame(template, targetSystem.getContext().getRequiredDependencyValue(ReactiveMongoTemplate.class));
+ assertSame(database, targetSystem.getMongoDatabase());
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java
new file mode 100644
index 000000000..8ce3e9fda
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/MongoDBTestHelper.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
+import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
+import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle;
+import io.flamingock.internal.common.mongodb.CollectionInitializator;
+import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBReactiveCollectionHelper;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import org.bson.Document;
+
+import java.util.HashSet;
+import java.util.function.Predicate;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class MongoDBTestHelper {
+
+ public final MongoDatabase mongoDatabase;
+
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+
+ public MongoDBTestHelper(MongoDatabase mongoDatabase) {
+ this.mongoDatabase = mongoDatabase;
+ }
+
+ public void insertOngoingExecution(String changeId) {
+
+ MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME);
+
+ CollectionInitializator initializer = new CollectionInitializator<>(
+ new MongoDBReactiveCollectionHelper(onGoingChangesCollection),
+ () -> new MongoDBDocumentHelper(new Document()),
+ new String[]{"changeId"}
+ );
+ initializer.initialize();
+
+
+ Document filter = new Document("changeId", changeId);
+
+ Document newDocument = new Document("changeId", changeId)
+ .append("operation", AuditContextBundle.Operation.EXECUTION.toString());
+
+ ReactiveMongoTestHelper.complete(onGoingChangesCollection.updateOne(
+ filter,
+ new Document("$set", newDocument),
+ new com.mongodb.client.model.UpdateOptions().upsert(true)));
+
+ checkEmptyTargetSystemAudiMarker();
+ }
+
+ public void checkCount(MongoCollection collection, int count) {
+ long result = ReactiveMongoTestHelper.collect(collection.find()).stream().count();
+ assertEquals(count, (int) result);
+ }
+
+ public void checkEmptyTargetSystemAudiMarker() {
+ checkOngoingChange(result -> result == 0);
+ }
+
+ public void checkOngoingChange(Predicate predicate) {
+ MongoCollection onGoingChangesCollection = mongoDatabase.getCollection("flamingockOnGoingChanges");
+
+ long result = ReactiveMongoTestHelper.collect(onGoingChangesCollection.find())
+ .stream()
+ .map(MongoDBTestHelper::mapToOnGoingStatus)
+ .collect(java.util.stream.Collectors.toCollection(HashSet::new))
+ .size();
+
+ assertTrue(predicate.test(result));
+ }
+
+ public static TargetSystemAuditMark mapToOnGoingStatus(Document document) {
+ TargetSystemAuditMarkType operation = TargetSystemAuditMarkType.valueOf(document.getString("operation"));
+ return new TargetSystemAuditMark(document.getString("changeId"), operation);
+ }
+
+
+
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java
new file mode 100644
index 000000000..ddc1c5a31
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/PipelineTestHelper.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import io.flamingock.api.StageType;
+import io.flamingock.api.annotations.TargetSystem;
+import io.flamingock.internal.common.core.metadata.FlamingockMetadata;
+import io.flamingock.internal.common.core.preview.PreviewConstructor;
+import io.flamingock.internal.common.core.change.RecoveryDescriptor;
+import io.flamingock.internal.common.core.change.TargetSystemDescriptor;
+import io.flamingock.internal.core.change.loaded.ChangeOrderUtil;
+import io.flamingock.internal.util.Pair;
+import io.flamingock.internal.util.Trio;
+import io.flamingock.api.annotations.Change;
+import io.flamingock.internal.common.core.preview.CodePreviewChange;
+import io.flamingock.internal.common.core.preview.PreviewMethod;
+import io.flamingock.internal.common.core.preview.PreviewPipeline;
+import io.flamingock.internal.common.core.preview.PreviewStage;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class PipelineTestHelper {
+
+
+ private static final Function, ChangeInfo> infoExtractor = c -> {
+ Change ann = c.getAnnotation(Change.class);
+ TargetSystem targetSystemAnn = c.getAnnotation(TargetSystem.class);
+ String targetSystemId = targetSystemAnn != null ? targetSystemAnn.id() : null;
+ String changeId = ann.id();
+ String order = ChangeOrderUtil.getMatchedOrderFromClassName(changeId, null, c.getName());
+ return new ChangeInfo(changeId, order, ann.author(), targetSystemId, ann.transactional());
+ };
+
+ @NotNull
+ private static List getParameterTypes(List> second) {
+ return second
+ .stream()
+ .map(Class::getName)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Builds a {@link PreviewPipeline} composed of a single {@link PreviewStage} containing one or more {@link CodePreviewChange}s.
+ *
+ * Each change is derived from a {@link Pair} where:
+ *
+ * - The first item is the {@link Class} annotated with {@link Change}
+ * - The second item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Apply}
+ * - The third item is a {@link List} of parameter types (as {@link Class}) expected by the method annotated with {@code @Rollback}
+ *
+ *
+ * @param changeDefinitions varargs of pairs containing change classes and their execution method parameters
+ * @return a {@link PreviewPipeline} ready for preview or testing
+ */
+ @SafeVarargs
+ public static FlamingockMetadata getPreviewPipeline(String stageName, Trio, List>, List>>... changeDefinitions) {
+
+ List changes = Arrays.stream(changeDefinitions)
+ .map(trio -> {
+ Function, ChangeInfo> extractor = infoExtractor;
+ ChangeInfo changeInfo = extractor.apply(trio.getFirst());
+ PreviewMethod rollback = null;
+ if (trio.getThird() != null) {
+ rollback = new PreviewMethod("rollback", getParameterTypes(trio.getThird()));
+ }
+
+ List changeList = new ArrayList<>();
+ changeList.add(new CodePreviewChange(
+ changeInfo.getChangeId(),
+ changeInfo.getOrder(),
+ changeInfo.getAuthor(),
+ trio.getFirst().getName(),
+ null,
+ PreviewConstructor.getDefault(),
+ new PreviewMethod("apply", getParameterTypes(trio.getSecond())),
+ rollback,
+ false,
+ changeInfo.transactional,
+ false,
+ changeInfo.targetSystem,
+ RecoveryDescriptor.getDefault(),
+ false
+ ));
+ return changeList;
+ })
+ .flatMap(List::stream)
+ .collect(Collectors.toList());
+
+ PreviewStage stage = new PreviewStage(
+ stageName,
+ StageType.DEFAULT,
+ "some description",
+ null,
+ null,
+ changes
+ );
+
+ PreviewPipeline previewPipeline = new PreviewPipeline(Collections.singletonList(stage));
+ return new FlamingockMetadata(previewPipeline, null, null);
+ }
+
+ @SafeVarargs
+ public static FlamingockMetadata getPreviewPipeline(Trio, List>, List>>... changeDefinitions) {
+ return getPreviewPipeline("default-stage-name", changeDefinitions);
+ }
+
+ static class ChangeInfo {
+ private final String changeId;
+ private final String order;
+ private final String author;
+ private final TargetSystemDescriptor targetSystem;
+ private final boolean transactional;
+
+ public ChangeInfo(String changeId, String order, String author, String targetSystemId, boolean transactional) {
+ this.changeId = changeId;
+ this.order = order;
+ this.author = author;
+ this.targetSystem = new TargetSystemDescriptor(targetSystemId);
+ this.transactional = transactional;
+ }
+
+ public String getChangeId() {
+ return changeId;
+ }
+
+ public String getOrder() {
+ return order;
+ }
+
+ public String getAuthor() { return author; }
+
+ public TargetSystemDescriptor getTargetSystem() {
+ return targetSystem;
+ }
+
+ public boolean isTransactional() {
+ return transactional;
+ }
+ }
+
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java
new file mode 100644
index 000000000..98b4efd0a
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/ReactiveMongoTestHelper.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive;
+
+import io.flamingock.internal.common.core.error.FlamingockException;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class ReactiveMongoTestHelper {
+
+ private ReactiveMongoTestHelper() {
+ }
+
+ public static T first(Publisher publisher) {
+ List values = collect(publisher);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ public static List collect(Publisher publisher) {
+ List values = new ArrayList<>();
+ AtomicReference error = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ publisher.subscribe(new Subscriber() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T value) {
+ values.add(value);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error.set(throwable);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlamingockException(e);
+ }
+ Throwable throwable = error.get();
+ if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ }
+ if (throwable != null) {
+ throw new FlamingockException(throwable);
+ }
+ return values;
+ }
+
+ public static void complete(Publisher> publisher) {
+ collect(publisher);
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java
new file mode 100644
index 000000000..8540ce382
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_001__HappyCreateClientsCollectionChange.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false)
+public class _001__HappyCreateClientsCollectionChange {
+
+ @Apply
+ public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) {
+ mongoTemplate.createCollection("clientCollection").block();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java
new file mode 100644
index 000000000..ef5dab84c
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/happypath/_002__HappyInsertClientsChange.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.happypath;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "insert-clients", author = "aperezdieppa")
+public class _002__HappyInsertClientsChange {
+
+ @Apply
+ public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) {
+ mongoTemplate.insert(new Document().append("Name", "Federico"), "clientCollection").block();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java
new file mode 100644
index 000000000..fb6cfad13
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_001__UnhappyCreateClientsCollectionChange.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "create-clients-collection", author = "aperezdieppa", transactional = false)
+public class _001__UnhappyCreateClientsCollectionChange {
+
+ @Apply
+ public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate) {
+ mongoTemplate.createCollection("clientCollection").block();
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java
new file mode 100644
index 000000000..bb7e39504
--- /dev/null
+++ b/core/target-systems/flamingock-mongodb-springdata-reactive-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/reactive/changes/unhappypath/_002__UnhappyInsertClientsChange.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.targetsystem.mongodb.springdata.reactive.changes.unhappypath;
+
+import io.flamingock.api.annotations.Change;
+import io.flamingock.api.annotations.Apply;
+import io.flamingock.api.annotations.NonLockGuarded;
+import io.flamingock.api.annotations.TargetSystem;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+@TargetSystem( id = "mongodb-ts")
+@Change(id = "insert-clients", author = "aperezdieppa")
+public class _002__UnhappyInsertClientsChange {
+
+ @Apply
+ public void apply(@NonLockGuarded ReactiveMongoTemplate mongoTemplate
+ ) {
+ mongoTemplate.insert(new Document().append("name", "Should Have Been Rolled Back"), "clientCollection").block();
+ throw new RuntimeException("Intended exception");
+ }
+}
diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts
index 90fd725ae..e20c51802 100644
--- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts
+++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/build.gradle.kts
@@ -10,6 +10,7 @@ dependencies {
//Flamingock
api(project(":core:flamingock-core"))
implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:mongodb-sync-util"))
implementation(project(":legacy:mongock-importer-mongodb"))
api(project(":core:target-systems:flamingock-mongodb-externalsystem-api"))
diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java
index 7f460d310..f45f0e133 100644
--- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java
+++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBSpringDataAuditMarker.java
@@ -22,7 +22,7 @@
import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
import io.flamingock.internal.common.mongodb.CollectionInitializator;
import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker;
import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
@@ -113,9 +113,9 @@ public Builder withAutoCreate(boolean autoCreate) {
public MongoDBSpringDataAuditMarker build() {
MongoDatabase db = mongoTemplate.getDb();
MongoCollection collection = db.getCollection(collectionName);
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(collection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{CHANGE_ID}
);
if (autoCreate) {
diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java
index 422e69a99..02d84d8af 100644
--- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java
+++ b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/MongoDBTestHelper.java
@@ -22,9 +22,9 @@
import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle;
import io.flamingock.internal.common.mongodb.CollectionInitializator;
import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
-import io.flamingock.targetsystem.mongodb.springdata.util.MongoDBSyncCollectionHelper;
-import io.flamingock.targetsystem.mongodb.springdata.util.MongoDBSyncDocumentHelper;
import org.bson.Document;
import java.util.HashSet;
@@ -37,7 +37,7 @@ public class MongoDBTestHelper {
public final MongoDatabase mongoDatabase;
- private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document()));
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
public MongoDBTestHelper(MongoDatabase mongoDatabase) {
this.mongoDatabase = mongoDatabase;
@@ -47,9 +47,9 @@ public void insertOngoingExecution(String changeId) {
MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME);
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(onGoingChangesCollection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{"changeId"}
);
initializer.initialize();
diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java
deleted file mode 100644
index 9e7b4b5c3..000000000
--- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncCollectionHelper.java
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2023 Flamingock (https://www.flamingock.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.flamingock.targetsystem.mongodb.springdata.util;
-
-import com.mongodb.client.MongoCollection;
-import com.mongodb.client.model.IndexOptions;
-import io.flamingock.internal.common.mongodb.CollectionHelper;
-import io.flamingock.internal.common.mongodb.DocumentHelper;
-import org.bson.Document;
-
-
-public class MongoDBSyncCollectionHelper implements CollectionHelper {
-
- private final MongoCollection collection;
-
-
- public MongoDBSyncCollectionHelper(MongoCollection collection) {
- this.collection = collection;
- }
-
- @Override
- public String getCollectionName() {
- return collection.getNamespace().getCollectionName();
- }
-
- @Override
- public Iterable listIndexes() {
- return collection.listIndexes().map(MongoDBSyncDocumentHelper::new);
- }
-
- @Override
- public String createUniqueIndex(MongoDBSyncDocumentHelper uniqueIndexDocument) {
- return collection.createIndex(uniqueIndexDocument.getDocument(), new IndexOptions().unique(true));
- }
-
- @Override
- public void dropIndex(String indexName) {
- collection.dropIndex(indexName);
- }
-
- @Override
- public void deleteMany(MongoDBSyncDocumentHelper documentWrapper) {
- collection.deleteMany(documentWrapper.getDocument());
- }
-
-
-}
diff --git a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java b/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java
deleted file mode 100644
index 07dda7ccc..000000000
--- a/core/target-systems/flamingock-mongodb-springdata-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/springdata/util/MongoDBSyncDocumentHelper.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright 2023 Flamingock (https://www.flamingock.io)
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package io.flamingock.targetsystem.mongodb.springdata.util;
-
-import io.flamingock.internal.common.mongodb.DocumentHelper;
-import org.bson.Document;
-
-public class MongoDBSyncDocumentHelper implements DocumentHelper {
-
- private final Document document;
-
- public MongoDBSyncDocumentHelper(Document document) {
- this.document = document;
- }
-
- public Document getDocument() {
- return document;
- }
-
- @Override
- public DocumentHelper append(String key, Object value) {
- document.append(key, value);
- return this;
- }
-
- @Override
- public Object get(String key) {
- return document.get(key);
- }
-
- @Override
- public String getString(String key) {
- return document.getString(key);
- }
-
- @Override
- public boolean containsKey(String key) {
- return document.containsKey(key);
- }
-
- @Override
- public Boolean getBoolean(String key) {
- return document.getBoolean(key);
- }
-
- public boolean getBoolean(Object key, boolean defaultValue) {
- return document.getBoolean(key, defaultValue);
- }
-
- @Override
- public int size() {
- return document.size();
- }
-
- @Override
- public DocumentHelper getWithWrapper(String key) {
- return new MongoDBSyncDocumentHelper((Document) get(key));
- }
-}
diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts b/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts
index be504b274..5a22f2b21 100644
--- a/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts
+++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/build.gradle.kts
@@ -3,6 +3,7 @@ dependencies {
//Flamingock
api(project(":core:flamingock-core"))
implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:mongodb-sync-util"))
implementation(project(":legacy:mongock-importer-mongodb"))
api(project(":core:target-systems:flamingock-mongodb-externalsystem-api"))
@@ -27,4 +28,4 @@ java {
configurations.testImplementation {
extendsFrom(configurations.compileOnly.get())
-}
\ No newline at end of file
+}
diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java
index d0d927366..6b6d563d6 100644
--- a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java
+++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/main/java/io/flamingock/targetsystem/mongodb/sync/MongoDBSyncAuditMarker.java
@@ -25,7 +25,7 @@
import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
import io.flamingock.internal.common.mongodb.CollectionInitializator;
import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMarker;
import io.flamingock.internal.core.transaction.TransactionManager;
@@ -133,9 +133,9 @@ public MongoDBSyncAuditMarker build() {
// .withWriteConcern(writeConcern)
;
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(collection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{CHANGE_ID}
);
if (autoCreate) {
diff --git a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java
index 0cc38f536..a881eb075 100644
--- a/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java
+++ b/core/target-systems/flamingock-mongodb-sync-targetsystem/src/test/java/io/flamingock/targetsystem/mongodb/sync/MongoDBTestHelper.java
@@ -19,7 +19,7 @@
import com.mongodb.client.MongoDatabase;
import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
import io.flamingock.internal.common.mongodb.MongoDBSyncCollectionHelper;
-import io.flamingock.internal.common.mongodb.MongoDBSyncDocumentHelper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
import io.flamingock.internal.common.core.targets.TargetSystemAuditMarkType;
import io.flamingock.internal.core.external.targets.mark.TargetSystemAuditMark;
import io.flamingock.internal.core.external.store.audit.domain.AuditContextBundle;
@@ -37,7 +37,7 @@ public class MongoDBTestHelper {
public final MongoDatabase mongoDatabase;
- private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBSyncDocumentHelper(new Document()));
+ private final MongoDBAuditMapper mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
public MongoDBTestHelper(MongoDatabase mongoDatabase) {
this.mongoDatabase = mongoDatabase;
@@ -47,9 +47,9 @@ public void insertOngoingExecution(String changeId) {
MongoCollection onGoingChangesCollection = mongoDatabase.getCollection(CommunityPersistenceConstants.DEFAULT_MARKER_STORE_NAME);
- CollectionInitializator initializer = new CollectionInitializator<>(
+ CollectionInitializator initializer = new CollectionInitializator<>(
new MongoDBSyncCollectionHelper(onGoingChangesCollection),
- () -> new MongoDBSyncDocumentHelper(new Document()),
+ () -> new MongoDBDocumentHelper(new Document()),
new String[]{"changeId"}
);
initializer.initialize();
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 43dfc69c8..55f05cff1 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -61,6 +61,11 @@ project(":community:flamingock-mongodb-sync-auditstore").name = "flamingock-mong
project(":community:flamingock-mongodb-sync-auditstore").projectDir =
file("community/flamingock-mongodb-sync-auditstore")
+include("community:flamingock-mongodb-reactive-auditstore")
+project(":community:flamingock-mongodb-reactive-auditstore").name = "flamingock-mongodb-reactive-auditstore"
+project(":community:flamingock-mongodb-reactive-auditstore").projectDir =
+ file("community/flamingock-mongodb-reactive-auditstore")
+
include("community:flamingock-couchbase-auditstore")
project(":community:flamingock-couchbase-auditstore").name = "flamingock-couchbase-auditstore"
project(":community:flamingock-couchbase-auditstore").projectDir = file("community/flamingock-couchbase-auditstore")
@@ -93,10 +98,16 @@ include("core:target-systems:flamingock-nontransactional-targetsystem")
include("core:target-systems:flamingock-mongodb-externalsystem-api")
+include("core:target-systems:flamingock-mongodb-reactive-externalsystem-api")
+
include("core:target-systems:flamingock-mongodb-sync-targetsystem")
+include("core:target-systems:flamingock-mongodb-reactive-targetsystem")
+
include("core:target-systems:flamingock-mongodb-springdata-targetsystem")
+include("core:target-systems:flamingock-mongodb-springdata-reactive-targetsystem")
+
include("core:target-systems:flamingock-sql-externalsystem-api")
include("core:target-systems:flamingock-sql-targetsystem")
@@ -122,10 +133,25 @@ include("utils:mongodb-util")
project(":utils:mongodb-util").name = "mongodb-util"
project(":utils:mongodb-util").projectDir = file("utils/mongodb-util")
+include("utils:mongodb-sync-util")
+project(":utils:mongodb-sync-util").name = "mongodb-sync-util"
+project(":utils:mongodb-sync-util").projectDir = file("utils/mongodb-sync-util")
+
+include("utils:mongodb-reactive-util")
+project(":utils:mongodb-reactive-util").name = "mongodb-reactive-util"
+project(":utils:mongodb-reactive-util").projectDir = file("utils/mongodb-reactive-util")
+
+include("utils:flamingock-reactive-util")
+project(":utils:flamingock-reactive-util").projectDir = file("utils/reactive-util")
+
include("utils:mongodb-test-kit")
project(":utils:mongodb-test-kit").name = "mongodb-test-kit"
project(":utils:mongodb-test-kit").projectDir = file("utils/mongodb-test-kit")
+include("utils:mongodb-reactive-test-kit")
+project(":utils:mongodb-reactive-test-kit").name = "mongodb-reactive-test-kit"
+project(":utils:mongodb-reactive-test-kit").projectDir = file("utils/mongodb-reactive-test-kit")
+
include("utils:dynamodb-util")
project(":utils:dynamodb-util").name = "dynamodb-util"
project(":utils:dynamodb-util").projectDir = file("utils/dynamodb-util")
diff --git a/utils/mongodb-reactive-test-kit/build.gradle.kts b/utils/mongodb-reactive-test-kit/build.gradle.kts
new file mode 100644
index 000000000..8433860d2
--- /dev/null
+++ b/utils/mongodb-reactive-test-kit/build.gradle.kts
@@ -0,0 +1,20 @@
+dependencies {
+ implementation(project(":core:flamingock-core"))
+ implementation(project(":utils:mongodb-util"))
+ implementation(project(":utils:flamingock-reactive-util"))
+ implementation(project(":utils:test-util"))
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0")
+}
+
+description = "MongoDB reactive streams TestKit for Flamingock testing"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java
new file mode 100644
index 000000000..f889e59b5
--- /dev/null
+++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveAuditStorage.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.mongodb.reactive.kit;
+
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.core.kit.audit.AuditStorage;
+import io.flamingock.internal.common.core.audit.AuditEntry;
+import io.flamingock.internal.common.mongodb.MongoDBAuditMapper;
+import io.flamingock.internal.common.mongodb.MongoDBDocumentHelper;
+import io.flamingock.internal.util.constants.CommunityPersistenceConstants;
+import io.flamingock.reactive.util.PublisherSync;
+import org.bson.Document;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_CHANGE_ID;
+import static io.flamingock.internal.util.constants.AuditEntryFieldConstants.KEY_STATE;
+
+/**
+ * MongoDB reactive implementation of AuditStorage for real database testing.
+ * Only depends on MongoDB client/database and core Flamingock classes.
+ * Does not depend on MongoDB-specific Flamingock components like MongoDBReactiveTargetSystem.
+ */
+public class MongoDBReactiveAuditStorage implements AuditStorage {
+
+ private static final String AUDIT_COLLECTION_NAME = CommunityPersistenceConstants.DEFAULT_AUDIT_STORE_NAME;
+
+ private final MongoCollection auditCollection;
+ private final MongoDBAuditMapper mapper;
+
+ public MongoDBReactiveAuditStorage(MongoDatabase database) {
+ this(database, AUDIT_COLLECTION_NAME);
+ }
+
+ public MongoDBReactiveAuditStorage(MongoDatabase database, String collectionName) {
+ this.auditCollection = database.getCollection(collectionName);
+ this.mapper = new MongoDBAuditMapper<>(() -> new MongoDBDocumentHelper(new Document()));
+ }
+
+ @Override
+ public void addAuditEntry(AuditEntry auditEntry) {
+ MongoDBDocumentHelper documentHelper = mapper.toDocument(auditEntry);
+ PublisherSync.complete(auditCollection.insertOne(documentHelper.getDocument()));
+ }
+
+ @Override
+ public List getAuditEntries() {
+ return PublisherSync.collect(auditCollection.find())
+ .stream()
+ .map(doc -> mapper.fromDocument(new MongoDBDocumentHelper(doc)))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public List getAuditEntriesForChange(String changeId) {
+ return PublisherSync.collect(auditCollection.find(new Document(KEY_CHANGE_ID, changeId)))
+ .stream()
+ .map(doc -> mapper.fromDocument(new MongoDBDocumentHelper(doc)))
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public long countAuditEntriesWithStatus(AuditEntry.Status status) {
+ Long count = PublisherSync.first(auditCollection.countDocuments(new Document(KEY_STATE, status.toString())));
+ return count != null ? count : 0L;
+ }
+
+ @Override
+ public boolean hasAuditEntries() {
+ Long count = PublisherSync.first(auditCollection.countDocuments());
+ return count != null && count > 0;
+ }
+
+ @Override
+ public void clear() {
+ PublisherSync.complete(auditCollection.deleteMany(new Document()));
+ }
+}
diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java
new file mode 100644
index 000000000..b4bd699ef
--- /dev/null
+++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveLockStorage.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.mongodb.reactive.kit;
+
+import com.mongodb.client.model.ReplaceOptions;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.core.kit.lock.LockStorage;
+import io.flamingock.internal.core.external.store.lock.LockAcquisition;
+import io.flamingock.internal.core.external.store.lock.LockKey;
+import io.flamingock.internal.util.id.RunnerId;
+import io.flamingock.reactive.util.PublisherSync;
+import org.bson.Document;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import static io.flamingock.internal.util.constants.CommunityPersistenceConstants.DEFAULT_LOCK_STORE_NAME;
+
+/**
+ * MongoDB reactive implementation of LockStorage for real database testing.
+ * Only depends on MongoDB client/database and core Flamingock classes.
+ * Does not depend on MongoDB-specific Flamingock components like MongoDBReactiveTargetSystem.
+ */
+public class MongoDBReactiveLockStorage implements LockStorage {
+
+ private final MongoCollection lockCollection;
+ private final MongoCollection metadataCollection;
+
+ public MongoDBReactiveLockStorage(MongoDatabase database) {
+ this(database, DEFAULT_LOCK_STORE_NAME);
+ }
+
+ public MongoDBReactiveLockStorage(MongoDatabase database, String lockCollectionName) {
+ this.lockCollection = database.getCollection(lockCollectionName);
+ this.metadataCollection = database.getCollection(lockCollectionName + "Metadata");
+ }
+
+ @Override
+ public void storeLock(LockKey key, LockAcquisition acquisition) {
+ Document lockDoc = new Document()
+ .append("_id", key.toString())
+ .append("key", key.toString())
+ .append("owner", acquisition.getOwner().getKey())
+ .append("leaseMillis", acquisition.getAcquiredForMillis())
+ .append("createdAt", new Date());
+
+ PublisherSync.complete(lockCollection.replaceOne(
+ new Document("_id", key.toString()),
+ lockDoc,
+ new ReplaceOptions().upsert(true)
+ ));
+ }
+
+ @Override
+ public LockAcquisition getLockInfo(LockKey key) {
+ Document doc = PublisherSync.first(lockCollection.find(new Document("_id", key.toString())).first());
+ if (doc == null) {
+ return null;
+ }
+
+ return documentToLockAcquisition(doc);
+ }
+
+ @Override
+ public Map getAllLocks() {
+ Map locks = new HashMap<>();
+
+ for (Document doc : PublisherSync.collect(lockCollection.find())) {
+ LockKey key = LockKey.fromString(doc.getString("key"));
+ LockAcquisition acquisition = documentToLockAcquisition(doc);
+ locks.put(key, acquisition);
+ }
+
+ return locks;
+ }
+
+ @Override
+ public void removeLock(LockKey key) {
+ PublisherSync.complete(lockCollection.deleteOne(new Document("_id", key.toString())));
+ }
+
+ @Override
+ public boolean hasLocks() {
+ Long count = PublisherSync.first(lockCollection.countDocuments());
+ return count != null && count > 0;
+ }
+
+ @Override
+ public void clear() {
+ PublisherSync.complete(lockCollection.deleteMany(new Document()));
+ PublisherSync.complete(metadataCollection.deleteMany(new Document()));
+ }
+
+ @Override
+ public void setMetadata(String key, Object value) {
+ Document metadataDoc = new Document()
+ .append("_id", key)
+ .append("key", key)
+ .append("value", value);
+
+ PublisherSync.complete(metadataCollection.replaceOne(
+ new Document("_id", key),
+ metadataDoc,
+ new ReplaceOptions().upsert(true)
+ ));
+ }
+
+ @Override
+ public Object getMetadata(String key) {
+ Document doc = PublisherSync.first(metadataCollection.find(new Document("_id", key)).first());
+ return doc != null ? doc.get("value") : null;
+ }
+
+ private LockAcquisition documentToLockAcquisition(Document doc) {
+ RunnerId owner = RunnerId.fromString(doc.getString("owner"));
+ long leaseMillis = doc.getLong("leaseMillis");
+
+ return new LockAcquisition(owner, leaseMillis);
+ }
+}
diff --git a/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java
new file mode 100644
index 000000000..b494d8fea
--- /dev/null
+++ b/utils/mongodb-reactive-test-kit/src/main/java/io/flamingock/mongodb/reactive/kit/MongoDBReactiveTestKit.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.mongodb.reactive.kit;
+
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+import io.flamingock.core.kit.AbstractTestKit;
+import io.flamingock.core.kit.audit.AuditStorage;
+import io.flamingock.core.kit.lock.LockStorage;
+import io.flamingock.internal.core.external.store.CommunityAuditStore;
+import io.flamingock.reactive.util.PublisherSync;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public class MongoDBReactiveTestKit extends AbstractTestKit {
+ private static final Set SYSTEM_DATABASES =
+ new HashSet<>(Arrays.asList("admin", "config", "local"));
+
+ private final MongoClient mongoClient;
+
+ public MongoDBReactiveTestKit(AuditStorage auditStorage, LockStorage lockStorage, CommunityAuditStore auditStore, MongoClient mongoClient) {
+ super(auditStorage, lockStorage, auditStore);
+ this.mongoClient = mongoClient;
+ }
+
+ @Override
+ public void cleanUp() {
+ PublisherSync.collect(mongoClient.listDatabaseNames()).forEach(dbName -> {
+ if (!SYSTEM_DATABASES.contains(dbName)) {
+ PublisherSync.complete(mongoClient.getDatabase(dbName).drop());
+ }
+ });
+ }
+
+ /**
+ * Create a new MongoDBReactiveTestKit with MongoDB client and database.
+ */
+ public static MongoDBReactiveTestKit create(CommunityAuditStore auditStore, MongoClient mongoClient, MongoDatabase database) {
+ MongoDBReactiveAuditStorage auditStorage = new MongoDBReactiveAuditStorage(database);
+ MongoDBReactiveLockStorage lockStorage = new MongoDBReactiveLockStorage(database);
+ return new MongoDBReactiveTestKit(auditStorage, lockStorage, auditStore, mongoClient);
+ }
+}
diff --git a/utils/mongodb-reactive-util/build.gradle.kts b/utils/mongodb-reactive-util/build.gradle.kts
new file mode 100644
index 000000000..1e742461d
--- /dev/null
+++ b/utils/mongodb-reactive-util/build.gradle.kts
@@ -0,0 +1,18 @@
+dependencies {
+ implementation(project(":utils:mongodb-util"))
+ implementation(project(":core:flamingock-core-commons"))
+
+ compileOnly("org.mongodb:mongodb-driver-reactivestreams:4.0.0")
+}
+
+description = "MongoDB reactive streams driver utilities"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java b/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java
new file mode 100644
index 000000000..a16284cc0
--- /dev/null
+++ b/utils/mongodb-reactive-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBReactiveCollectionHelper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.internal.common.mongodb;
+
+import com.mongodb.client.model.IndexOptions;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import io.flamingock.internal.common.core.error.FlamingockException;
+import org.bson.Document;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.List;
+import java.util.ArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+public class MongoDBReactiveCollectionHelper implements CollectionHelper {
+
+ private final MongoCollection collection;
+
+ public MongoDBReactiveCollectionHelper(MongoCollection collection) {
+ this.collection = collection;
+ }
+
+ @Override
+ public String getCollectionName() {
+ return collection.getNamespace().getCollectionName();
+ }
+
+ @Override
+ public Iterable listIndexes() {
+ List indexes = collect(collection.listIndexes());
+ return indexes.stream().map(MongoDBDocumentHelper::new).collect(Collectors.toList());
+ }
+
+ @Override
+ public String createUniqueIndex(MongoDBDocumentHelper uniqueIndexDocument) {
+ return first(collection.createIndex(
+ uniqueIndexDocument.getDocument(),
+ new IndexOptions().unique(true)));
+ }
+
+ @Override
+ public void dropIndex(String indexName) {
+ complete(collection.dropIndex(indexName));
+ }
+
+ @Override
+ public void deleteMany(MongoDBDocumentHelper documentWrapper) {
+ first(collection.deleteMany(documentWrapper.getDocument()));
+ }
+
+ private static T first(Publisher publisher) {
+ List values = collect(publisher);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ private static List collect(Publisher publisher) {
+ List values = new ArrayList<>();
+ AtomicReference error = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ publisher.subscribe(new Subscriber() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T value) {
+ values.add(value);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error.set(throwable);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() {
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new FlamingockException(e);
+ }
+ Throwable throwable = error.get();
+ if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ }
+ if (throwable != null) {
+ throw new FlamingockException(throwable);
+ }
+ return values;
+ }
+
+ private static void complete(Publisher> publisher) {
+ collect(publisher);
+ }
+}
diff --git a/utils/mongodb-sync-util/build.gradle.kts b/utils/mongodb-sync-util/build.gradle.kts
new file mode 100644
index 000000000..734db3514
--- /dev/null
+++ b/utils/mongodb-sync-util/build.gradle.kts
@@ -0,0 +1,17 @@
+dependencies {
+ implementation(project(":utils:mongodb-util"))
+
+ compileOnly("org.mongodb:mongodb-driver-sync:4.0.0")
+}
+
+description = "MongoDB synchronous driver utilities"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java b/utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java
similarity index 82%
rename from utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java
rename to utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java
index 98a36839d..01993b567 100644
--- a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java
+++ b/utils/mongodb-sync-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncCollectionHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Flamingock (https://www.flamingock.io)
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
import org.bson.Document;
-public class MongoDBSyncCollectionHelper implements CollectionHelper {
+public class MongoDBSyncCollectionHelper implements CollectionHelper {
private final MongoCollection collection;
@@ -36,11 +36,11 @@ public String getCollectionName() {
@Override
public Iterable listIndexes() {
- return collection.listIndexes().map(MongoDBSyncDocumentHelper::new);
+ return collection.listIndexes().map(MongoDBDocumentHelper::new);
}
@Override
- public String createUniqueIndex(MongoDBSyncDocumentHelper uniqueIndexDocument) {
+ public String createUniqueIndex(MongoDBDocumentHelper uniqueIndexDocument) {
return collection.createIndex(uniqueIndexDocument.getDocument(), new IndexOptions().unique(true));
}
@@ -50,7 +50,7 @@ public void dropIndex(String indexName) {
}
@Override
- public void deleteMany(MongoDBSyncDocumentHelper documentWrapper) {
+ public void deleteMany(MongoDBDocumentHelper documentWrapper) {
collection.deleteMany(documentWrapper.getDocument());
}
diff --git a/utils/mongodb-util/build.gradle.kts b/utils/mongodb-util/build.gradle.kts
index dc40279d3..7e860f249 100644
--- a/utils/mongodb-util/build.gradle.kts
+++ b/utils/mongodb-util/build.gradle.kts
@@ -1,7 +1,7 @@
dependencies {
implementation(project(":core:flamingock-core"))
- compileOnly("org.mongodb:mongodb-driver-sync:4.0.0")
+ compileOnly("org.mongodb:bson:4.0.0")
testImplementation(project(":utils:test-util"))
}
@@ -16,4 +16,4 @@ java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(8))
}
-}
\ No newline at end of file
+}
diff --git a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java b/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java
similarity index 86%
rename from utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java
rename to utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java
index b71092771..dd912fd94 100644
--- a/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBSyncDocumentHelper.java
+++ b/utils/mongodb-util/src/main/java/io/flamingock/internal/common/mongodb/MongoDBDocumentHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2023 Flamingock (https://www.flamingock.io)
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,11 +17,11 @@
import org.bson.Document;
-public class MongoDBSyncDocumentHelper implements DocumentHelper {
+public class MongoDBDocumentHelper implements DocumentHelper {
private final Document document;
- public MongoDBSyncDocumentHelper(Document document) {
+ public MongoDBDocumentHelper(Document document) {
this.document = document;
}
@@ -66,6 +66,6 @@ public int size() {
@Override
public DocumentHelper getWithWrapper(String key) {
- return new MongoDBSyncDocumentHelper((Document) get(key));
+ return new MongoDBDocumentHelper((Document) get(key));
}
}
diff --git a/utils/reactive-util/build.gradle.kts b/utils/reactive-util/build.gradle.kts
new file mode 100644
index 000000000..894737f94
--- /dev/null
+++ b/utils/reactive-util/build.gradle.kts
@@ -0,0 +1,18 @@
+dependencies {
+ compileOnly("org.reactivestreams:reactive-streams:1.0.3")
+
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2")
+ testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2")
+}
+
+description = "Reactive streams utilities for Flamingock"
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(8))
+ }
+}
+
+configurations.testImplementation {
+ extendsFrom(configurations.compileOnly.get())
+}
diff --git a/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java
new file mode 100644
index 000000000..83310bfd4
--- /dev/null
+++ b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSync.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.reactive.util;
+
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscriber;
+import org.reactivestreams.Subscription;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
+
+public final class PublisherSync {
+
+ private PublisherSync() {
+ }
+
+ public static T first(Publisher publisher) {
+ List values = collect(publisher);
+ return values.isEmpty() ? null : values.get(0);
+ }
+
+ public static List collect(Publisher publisher) {
+ List values = new ArrayList<>();
+ await(publisher, values::add);
+ return values;
+ }
+
+ public static void complete(Publisher> publisher) {
+ await(publisher, value -> {
+ });
+ }
+
+ private static void await(Publisher publisher, ItemConsumer consumer) {
+ AtomicReference error = new AtomicReference<>();
+ CountDownLatch latch = new CountDownLatch(1);
+ publisher.subscribe(new Subscriber() {
+ @Override
+ public void onSubscribe(Subscription subscription) {
+ subscription.request(Long.MAX_VALUE);
+ }
+
+ @Override
+ public void onNext(T value) {
+ consumer.accept(value);
+ }
+
+ @Override
+ public void onError(Throwable throwable) {
+ error.set(throwable);
+ latch.countDown();
+ }
+
+ @Override
+ public void onComplete() {
+ latch.countDown();
+ }
+ });
+ await(latch);
+ Throwable throwable = error.get();
+ if (throwable instanceof RuntimeException) {
+ throw (RuntimeException) throwable;
+ }
+ if (throwable != null) {
+ throw new PublisherSyncException(throwable);
+ }
+ }
+
+ private static void await(CountDownLatch latch) {
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new PublisherSyncException(e);
+ }
+ }
+
+ private interface ItemConsumer {
+ void accept(T value);
+ }
+}
diff --git a/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java
new file mode 100644
index 000000000..74e0cd483
--- /dev/null
+++ b/utils/reactive-util/src/main/java/io/flamingock/reactive/util/PublisherSyncException.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.reactive.util;
+
+public class PublisherSyncException extends RuntimeException {
+
+ public PublisherSyncException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java b/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java
new file mode 100644
index 000000000..9e495a546
--- /dev/null
+++ b/utils/reactive-util/src/test/java/io/flamingock/reactive/util/PublisherSyncTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2026 Flamingock (https://www.flamingock.io)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.flamingock.reactive.util;
+
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscription;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class PublisherSyncTest {
+
+ @Test
+ void collectsPublisherValuesInOrder() {
+ assertEquals(Arrays.asList("first", "second"), PublisherSync.collect(values("first", "second")));
+ }
+
+ @Test
+ void returnsFirstPublisherValue() {
+ assertEquals("first", PublisherSync.first(values("first", "second")));
+ }
+
+ @Test
+ void returnsNullWhenPublisherIsEmpty() {
+ assertNull(PublisherSync.first(values()));
+ }
+
+ @Test
+ void completesPublisher() {
+ PublisherSync.complete(values("first", "second"));
+ }
+
+ @Test
+ void propagatesRuntimePublisherFailure() {
+ IllegalStateException failure = new IllegalStateException("failed");
+ IllegalStateException result = assertThrows(
+ IllegalStateException.class,
+ () -> PublisherSync.collect(error(failure)));
+ assertSame(failure, result);
+ }
+
+ @Test
+ void wrapsCheckedPublisherFailure() {
+ IOException failure = new IOException("failed");
+ PublisherSyncException result = assertThrows(
+ PublisherSyncException.class,
+ () -> PublisherSync.collect(error(failure)));
+ assertSame(failure, result.getCause());
+ }
+
+ @Test
+ void restoresInterruptedFlagWhenInterrupted() throws Exception {
+ AtomicReference failure = new AtomicReference<>();
+ AtomicReference interrupted = new AtomicReference<>(false);
+ Thread thread = new Thread(() -> {
+ try {
+ PublisherSync.complete(neverCompletes());
+ } catch (Throwable throwable) {
+ interrupted.set(Thread.currentThread().isInterrupted());
+ failure.set(throwable);
+ }
+ });
+
+ thread.start();
+ while (thread.getState() != Thread.State.WAITING) {
+ Thread.yield();
+ }
+ thread.interrupt();
+ thread.join(1000);
+
+ assertFalse(thread.isAlive());
+ assertTrue(interrupted.get());
+ assertTrue(failure.get() instanceof PublisherSyncException);
+ }
+
+ @SafeVarargs
+ private static Publisher values(T... values) {
+ return subscriber -> subscriber.onSubscribe(new Subscription() {
+ @Override
+ public void request(long count) {
+ for (T value : values) {
+ subscriber.onNext(value);
+ }
+ subscriber.onComplete();
+ }
+
+ @Override
+ public void cancel() {
+ }
+ });
+ }
+
+ private static Publisher error(Throwable failure) {
+ return subscriber -> subscriber.onSubscribe(new Subscription() {
+ @Override
+ public void request(long count) {
+ subscriber.onError(failure);
+ }
+
+ @Override
+ public void cancel() {
+ }
+ });
+ }
+
+ private static Publisher neverCompletes() {
+ return subscriber -> subscriber.onSubscribe(new Subscription() {
+ @Override
+ public void request(long count) {
+ }
+
+ @Override
+ public void cancel() {
+ }
+ });
+ }
+}