Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Data MongoDB

MongoDB Basics

  • Spring Data MongoDB
  • Indexing & Performance

Aggregation Pipeline

  • Aggregation Pipeline
  • Transactions in MongoDB
  • Schema Design Patterns
Chaturmind
← Spring Data MongoDB

MongoDB Basics

  • Spring Data MongoDB
  • Indexing & Performance

Aggregation Pipeline

  • Aggregation Pipeline
  • Transactions in MongoDB
  • Schema Design Patterns
HomeLearnSpring BootSpring Data MongoDBAggregation Pipeline
✓ FreeAdvanced· 7 min read

Transactions in MongoDB

Multi-document ACID transactions — when you need them and how to use them.

Published September 21, 2026


Transactions in MongoDB

A transaction groups several writes so they succeed or fail together. If any step fails, none of the changes become visible. That's the "A" (atomicity) in ACID.

In MongoDB the first thing to know is that every write to a single document is already atomic, including updates that change several fields, push into arrays and modify nested sub-documents. That's why well-designed MongoDB schemas need multi-document transactions much less often than relational ones: data that must change together is usually stored together in one document.

Multi-document transactions (MongoDB 4.0 for replica sets, 4.2 for sharded clusters) cover the remaining cases, where one business operation must update several documents or collections all-or-nothing.

When you actually need one

  • Moving money between two account documents: the debit and credit must both happen or neither.
  • Creating an order and reserving stock held in a separate inventory document, when that pairing must never be half-done.
  • Maintaining a document and a separate, strictly consistent summary or ledger entry.

When you don't need one:

  • Updating several fields of one document: already atomic.
  • Changes where eventual consistency is fine: an event plus an asynchronous consumer (see the saga pattern) often scales better.
  • "Just to be safe" around a single write: it adds overhead for no benefit.

Requirements

  • A replica set (or sharded cluster). Transactions do not work on a standalone mongod. For local development, run a single-node replica set, or use Testcontainers' MongoDBContainer, which starts one.
  • In Spring, a MongoTransactionManager bean. Without it, @Transactional silently does nothing for MongoDB operations.
@Configuration
public class MongoConfig {
    @Bean
    MongoTransactionManager transactionManager(MongoDatabaseFactory factory) {
        return new MongoTransactionManager(factory);
    }
}

Using @Transactional

@Service
@RequiredArgsConstructor
public class TransferService {

    private final MongoTemplate mongo;

    @Transactional
    public void transfer(String fromId, String toId, BigDecimal amount) {
        // Debit only if the balance is sufficient — the condition and the change are one atomic update
        UpdateResult debit = mongo.updateFirst(
                Query.query(Criteria.where("_id").is(fromId).and("balance").gte(amount)),
                new Update().inc("balance", amount.negate()),
                Account.class);

        if (debit.getModifiedCount() == 0) {
            // Throwing rolls back everything done in this transaction
            throw new InsufficientFundsException(fromId);
        }

        mongo.updateFirst(
                Query.query(Criteria.where("_id").is(toId)),
                new Update().inc("balance", amount),
                Account.class);

        mongo.insert(new LedgerEntry(fromId, toId, amount, Instant.now()));
    }
}

The check on getModifiedCount() is essential. An updateFirst whose filter matches nothing is not an error: it simply updates zero documents. Without that check, an account with too little money would silently not be debited, the credit would still happen, and money would be created from nothing. Always verify that each step actually did what the business operation needs, and throw to roll back if not.

The same rules as for any Spring @Transactional apply: the method must be called through the Spring proxy (not from another method in the same class), and by default only unchecked exceptions trigger a rollback.

What happens under the hood

  1. Spring starts a client session and a transaction on it. Every operation inside the method is sent with that session.
  2. Reads inside the transaction see a snapshot: a consistent view of the data as of the transaction's start, plus the transaction's own writes.
  3. Other clients don't see any of the writes until commit.
  4. On commit, all writes become visible at once. On abort (an exception), they are discarded.

Conflicts and retries

MongoDB transactions use optimistic concurrency. If two transactions modify the same document, one gets a WriteConflict error and is aborted. The driver labels such errors TransientTransactionError, meaning "safe to retry the whole transaction". Commits interrupted by network issues are labelled UnknownTransactionCommitResult, which means retry the commit. The callback API (session.withTransaction(...)) and Spring's TransactionTemplate handle these retries for you. With plain @Transactional you may want a retry around the whole method for hot documents.

// Explicit retries with the driver's callback API
try (ClientSession session = mongoClient.startSession()) {
    session.withTransaction(() -> {
        accounts.updateOne(session, eq("_id", fromId), inc("balance", amount.negate()));
        accounts.updateOne(session, eq("_id", toId), inc("balance", amount));
        return null;
    }, TransactionOptions.builder()
            .readConcern(ReadConcern.SNAPSHOT)
            .writeConcern(WriteConcern.MAJORITY)
            .build());
}

writeConcern: majority means the commit is acknowledged only once most replica-set members have it, so a primary failover can't lose a committed transaction. That's what you want for financial data.

Limits and costs

  • Runtime limit: by default a transaction is aborted if it runs longer than 60 seconds (transactionLifetimeLimitSeconds). Keep transactions short, and never call external services or wait for user input inside one.
  • Size: keep the number of documents modified per transaction modest (the guidance is to stay well under about 1,000). Large batches belong in bulk operations outside transactions.
  • Performance: transactions hold resources and create more write conflicts under contention. A workload built around them won't scale like one built on single-document atomic updates.
  • DDL: creating collections and indexes inside a transaction is possible since MongoDB 4.4, but only in limited cases (for example on an empty new collection). As a rule, create collections and indexes up front, outside transactions.

Design first, transaction second

Before reaching for a transaction, ask whether the schema can make the operation a single-document update:

  • Embed order lines in the order document instead of a separate order_lines collection.
  • Keep a counter or balance in the same document as the data it summarizes.
  • Use atomic update operators ($inc, $push, conditional filters like balance >= amount) instead of read-modify-write.

When the data genuinely has to live in separate documents or services, a saga (a sequence of local steps with compensating actions) is often better than a long distributed transaction, especially across microservices. That's covered in Failure Scenario Walkthroughs.

Follow-up questions this topic invites — and their answers

Q: Why doesn't @Transactional work in my local MongoDB setup? A: Two usual causes. There's no MongoTransactionManager bean, so Spring has nothing to manage MongoDB transactions with and the annotation does nothing. Or the server is a standalone mongod, which doesn't support transactions at all. Run a single-node replica set locally, or use Testcontainers.

Q: Are MongoDB transactions fully ACID? A: Yes, multi-document transactions provide atomicity, snapshot isolation for reads, and durability when committed with writeConcern: majority. The practical differences from relational databases are in cost and limits (runtime, size), and in the fact that good MongoDB schemas are designed to need them rarely.

Q: What isolation level do MongoDB transactions give? A: With readConcern: snapshot, reads inside the transaction see a consistent snapshot taken at the start, which is similar to snapshot isolation in relational databases. Concurrent writes to the same document cause a write conflict for one of the transactions, which then retries. You don't get lost updates between transactions.

Q: Would you use transactions to keep MongoDB and Kafka in sync? A: No. A MongoDB transaction can't include a Kafka publish. The standard answer is the transactional outbox: write the business change and an "event to publish" document in the same MongoDB transaction (or the same document), then a separate process (or change streams) publishes outbox events to Kafka and marks them sent. That gives at-least-once publishing without dual-write inconsistencies.

Previous

Aggregation Pipeline

Next

Schema Design Patterns

AI Tutor

Lesson: Transactions in MongoDB

Quick actions

AI responses can be inaccurate. Verify critical information.