Skip to main content

Bulk Writes

Bulk write operations combine insert, update, replace, and delete commands in one API call. The driver batches commands according to server limits, so one call can require multiple network round trips.

Use collection.bulkWrite for writes to one collection. Use client.bulkWrite for writes spanning collections or databases on the same MongoDB deployment; client-level bulk writes require MongoDB 8.0 or later.

WriteCommand types​

The WriteCommand sealed trait has the following variants:

CommandDescription
WriteCommand.InsertOne(document)Insert a single document
WriteCommand.UpdateOne(filter, update)Update the first matching document
WriteCommand.UpdateMany(filter, update)Update all matching documents
WriteCommand.ReplaceOne(filter, replacement)Replace the first matching document
WriteCommand.DeleteOne(filter)Delete the first matching document
WriteCommand.DeleteMany(filter)Delete all matching documents

Update, replace, and delete variants accept an optional options argument for upsert, collation, etc. InsertOne takes only its document.

Basic example​

import cats.effect.IO
import mongo4cats.bson.Document
import mongo4cats.bson.syntax._
import mongo4cats.client.MongoClient
import mongo4cats.models.collection.{BulkWriteOptions, WriteCommand}
import mongo4cats.operations.{Filter, Update}

MongoClient.fromConnectionString[IO]("mongodb://localhost:27017").use { client =>
for {
db <- client.getDatabase("mydb")
coll <- db.getCollection("products")

// Seed some data first
_ <- coll.insertMany((1 to 10).map(i => Document("name" := s"item-$i", "qty" := i * 10)))

commands = List(
WriteCommand.InsertOne(Document("name" := "item-11", "qty" := 110)),
WriteCommand.UpdateOne(Filter.eq("name", "item-1"), Update.set("qty", 999)),
WriteCommand.DeleteOne(Filter.eq("name", "item-2")),
WriteCommand.DeleteMany(Filter.gt("qty", 50) && Filter.lt("qty", 80)),
WriteCommand.ReplaceOne(Filter.eq("name", "item-10"), Document("name" := "item-10", "qty" := 0))
)

result <- coll.bulkWrite(commands)
_ <- IO.println(s"Inserted: ${result.getInsertedCount}")
_ <- IO.println(s"Modified: ${result.getModifiedCount}")
_ <- IO.println(s"Deleted: ${result.getDeletedCount}")
} yield ()
}

Bulk write options​

import mongo4cats.models.collection.BulkWriteOptions

// Ordered (default) — stop on first error
coll.bulkWrite(commands, BulkWriteOptions(ordered = true))

// Unordered — continue processing even if some commands fail
coll.bulkWrite(commands, BulkWriteOptions(ordered = false))

With ordered = true (the default), MongoDB executes commands in sequence and stops at the first error. With ordered = false, MongoDB may execute commands in parallel and reports all errors at the end; this is generally faster for independent writes.

Upsert on UpdateOne / UpdateMany​

import mongo4cats.models.collection.{UpdateOptions, WriteCommand}
import mongo4cats.operations.{Filter, Update}

val commands = List(
WriteCommand.UpdateOne(
Filter.eq("name", "new-item"),
Update.set("qty", 0),
UpdateOptions().upsert(true) // insert if not found
)
)

coll.bulkWrite(commands)

Inspecting BulkWriteResult​

import com.mongodb.bulk.BulkWriteResult

coll.bulkWrite(commands).map { result =>
println(s"Acknowledged: ${result.wasAcknowledged()}")
println(s"Inserted count: ${result.getInsertedCount}")
println(s"Matched count: ${result.getMatchedCount}")
println(s"Modified count: ${result.getModifiedCount}")
println(s"Deleted count: ${result.getDeletedCount}")
println(s"Upserted count: ${result.getUpserts.size}")
}

Using with a client session (transactions)​

client.startSession.use { session =>
for {
_ <- session.startTransaction
_ <- coll.bulkWrite(session, commands, BulkWriteOptions(ordered = true))
_ <- session.commitTransaction
} yield ()
}

Client-level bulk writes​

MongoClient and ZMongoClient accept a sequence of ClientWriteCommand values. Every command includes a MongoNamespace(databaseName, collectionName). Insert and replacement commands can contain different document types in the same sequence, provided the client has codecs for each type.

import cats.effect.IO
import mongo4cats.bson.Document
import mongo4cats.bson.syntax._
import mongo4cats.client.MongoClient
import mongo4cats.models.client.{ClientBulkWriteOptions, ClientWriteCommand}
import mongo4cats.models.collection.MongoNamespace
import mongo4cats.operations.{Filter, Update}

val products = MongoNamespace("shop", "products")
val events = MongoNamespace("audit", "events")

val commands: List[ClientWriteCommand] = List(
ClientWriteCommand.InsertOne(products, Document("sku" := "widget", "qty" := 10)),
ClientWriteCommand.UpdateOne(products, Filter.eq("sku", "widget"), Update.inc("qty", 5)),
ClientWriteCommand.InsertOne(events, Document("action" := "stock-added", "sku" := "widget"))
)

MongoClient.fromConnectionString[IO]("mongodb://localhost:27017").use { client =>
client.bulkWrite(commands, ClientBulkWriteOptions(verboseResults = true)).flatMap { result =>
IO.println(s"Inserted: ${result.getInsertedCount}, modified: ${result.getModifiedCount}")
}
}

The default ordered execution ensures that this example inserts the product before updating it. A bulk write is not atomic across operations: earlier successful writes are not rolled back if a later write fails. Passing a session alone does not start a transaction.

Commands and options​

The client commands mirror the collection commands, with the namespace as the first argument:

CommandArguments after the namespace
ClientWriteCommand.InsertOnedocument
ClientWriteCommand.UpdateOne / UpdateManyfilter, update, optional client update options
ClientWriteCommand.PipelinedUpdateOne / PipelinedUpdateManyfilter, pipeline Seq[Bson], optional client update options
ClientWriteCommand.ReplaceOnefilter, replacement, optional client replace options
ClientWriteCommand.DeleteOne / DeleteManyfilter, optional client delete options

Use the client-specific operation options from mongo4cats.models.client: ClientUpdateOneOptions, ClientUpdateManyOptions, ClientReplaceOneOptions, ClientDeleteOneOptions, and ClientDeleteManyOptions. Collection option types are not interchangeable with them. For example:

import mongo4cats.models.client.ClientUpdateOneOptions

ClientWriteCommand.UpdateOne(
products,
Filter.eq("sku", "new-widget"),
Update.set("qty", 10),
ClientUpdateOneOptions(upsert = true)
)

ClientBulkWriteOptions controls the whole request:

OptionDefaultMeaning
orderedtrueStop after the first individual write error. When false, individual write errors do not prevent other operations from executing.
verboseResultsfalseInclude results for individual successful operations.
bypassDocumentValidationfalseBypass collection document validation rules.
commentNoneAttach an optional string comment to the command.
letNoneSupply command-level variables as an optional BSON document.

Top-level failures, such as connection errors, can stop execution even when ordered = false. The driver handles batching and retry eligibility; avoid blindly retrying an entire failed bulk because some operations may already have succeeded.

Both effect implementations provide these four overloads:

client.bulkWrite(commands)
client.bulkWrite(commands, ClientBulkWriteOptions(ordered = false))
client.bulkWrite(session, commands)
client.bulkWrite(session, commands, ClientBulkWriteOptions(verboseResults = true))

The result is F[com.mongodb.client.model.bulk.ClientBulkWriteResult] for Cats Effect and Task[ClientBulkWriteResult] for ZIO. Sessions must belong to the same client. Use a replica set or sharded deployment for sessions and transactions; the embedded helpers start standalone servers.

Custom subclasses or test doubles of GenericMongoClient must implement the two new overloads that take explicit ClientBulkWriteOptions; the default-options overloads delegate to them.

Results and partial failures​

For an acknowledged result, getInsertedCount, getUpsertedCount, getMatchedCount, getModifiedCount, and getDeletedCount report totals across namespaces. Check isAcknowledged before accessing details if you configure unacknowledged write concern. Unacknowledged writes (w=0) require both ordered = false and verboseResults = false; the driver rejects other combinations.

getVerboseResults returns a Java Optional. With verboseResults = true, its insert, update, and delete result maps identify individual operations by their zero-based index in the original command sequence, including when the driver sends multiple batches. Map iteration order is unspecified.

Errors remain in the effect's error channel. The original com.mongodb.ClientBulkWriteException exposes:

  • getWriteErrors: individual write errors keyed by the original command index.
  • getWriteConcernErrors: write concern failures, which are separate from individual write errors.
  • getPartialResult: an optional result for operations known to have succeeded. It is present only when the driver received a response indicating success for at least one operation.
  • getCause: the top-level failure, or null when none occurred. Use its error code and labels when inspecting a top-level failure.

Some failures have no partial result, and failures without any write-error, write-concern-error, or partial-result information can surface as another MongoException. An absent partial result does not establish that no writes occurred.

For example, log the available information while keeping the failure visible to the caller:

import com.mongodb.ClientBulkWriteException

client.bulkWrite(commands, ClientBulkWriteOptions(ordered = false)).attempt.flatMap {
case Right(result) => IO.println(s"Inserted: ${result.getInsertedCount}")
case Left(error: ClientBulkWriteException) =>
val partial = error.getPartialResult
val inserted = if (partial.isPresent) Some(partial.get().getInsertedCount) else None
IO.println(
s"Write errors: ${error.getWriteErrors}; write concern errors: ${error.getWriteConcernErrors}; known inserted: $inserted"
).flatMap(_ => IO.raiseError[Unit](error))
case Left(error) => IO.raiseError[Unit](error)
}

Codecs for typed documents​

Client bulk writes use the client's codec registry. The mongo4cats client settings builder includes the library's default codecs, including mongo4cats.bson.Document. Codecs added only to a database or collection do not apply to client bulk writes.

For custom document types, merge their codec providers into the client registry before creating the client. For example, with the Circe module:

import cats.effect.IO
import io.circe.generic.auto._
import mongo4cats.circe._
import mongo4cats.client.MongoClient
import mongo4cats.codecs.CodecRegistry
import mongo4cats.models.client.{ConnectionString, MongoClientSettings}

case class Product(sku: String, qty: Int)

val provider = deriveCirceCodecProvider[Product]
val registry = CodecRegistry.mergeWithDefault(CodecRegistry.from(provider.get))
val settings = MongoClientSettings
.builder(codecRegistry = registry)
.applyConnectionString(ConnectionString("mongodb://localhost:27017"))
.build()

val clientResource = MongoClient.create[IO](settings)

The same registry configuration works with ZMongoClient.create(settings).