SENDLINE open source
open source / mongo4s / overview
No. 01now.sendline · Apache-2.0

mongo4s

MongoDB for Scala 3

Effect-agnostic MongoDB client and repository layer for Scala 3. No hardcoded cats-effect or fs2 — the runtime (cats-effect / ZIO / Kyo / rapid) and the BSON codec (your own derivation, or medeia / zio-bson / calypso) are independent modules, each wired in through a given import. core depends on neither.

Install

libraryDependencies += "now.sendline" %% "mongo4s-cats" % "6.0.0"

import mongo4s.cats.CatsInstances.given

The runtime you pick here is remembered on every page and in every library. mongo4s-kyo needs JDK 25 at runtime; every other module runs on JDK 17.

At a glance

mongo4s wraps the official mongodb-driver-reactivestreams directly. A type-safe Field/Filter/Update builder replaces string-keyed queries, PrimaryKey turns an entity into single- or compound-key lookups, and BaseMongoRepository gives you CRUD/batch operations over a collection for free. Every piece is interpretable against a real MongoDB and an in-memory FakeMongoCollection, so repositories are unit-testable without a running database.

flowchart LR
    app["your application"] --> repositories
    app --> core

    subgraph core["mongo4s-core"]
        CC["MongoClient · MongoDatabase · MongoCollection"]
        QQ["Field · Filter · Update · PrimaryKey"]
    end

    repositories["mongo4s-repositories<br/>BaseMongoRepository"] --> core
    RT["runtime<br/>cats-effect · ZIO · Kyo · rapid"] -->|given Effect, RsBridge| core
    WC["bson-direct<br/>WireCodec (AST-free)"] -->|given WireCodec| core
    BR["bson bridges<br/>medeia · zio-bson · calypso"] -->|given BsonDocumentCodec| core

Quick start

Pick a runtime. The default codec (bson-direct) derives straight from your case class — AST-free, no third-party codec library, nothing beyond mongo4s-core, which brings it in:

scala
libraryDependencies ++= Seq(
  "now.sendline" %% "mongo4s-cats"           % "6.0.0", // mongo4s-core + cats-effect integration
  "now.sendline" %% "mongo4s-bson-direct"    % "6.0.0", // ast-free bson codecs
  "now.sendline" %% "mongo4s-bson-cats-data" % "6.0.0", // if you need NonEmptyList etc. codec instances
  "now.sendline" %% "mongo4s-repositories"   % "6.0.0", // if you need auto-generated CRUD repository ops for your model
)
scala
import cats.effect.{IO, IOApp}

import mongo4s.cats.{CatsStream, MongoClientResource}
import mongo4s.bson.direct.WireCodec
import mongo4s.{Field, PrimaryKey}
import mongo4s.repositories.BaseMongoRepository

import mongo4s.cats.CatsInstances.given

final case class User(id: String, name: String, age: Int) derives WireCodec

object User:
  given PrimaryKey[User, String] = PrimaryKey.single("id")(_.id)

object Main extends IOApp.Simple:
  def run: IO[Unit] =
    MongoClientResource.fromConnectionString[IO]("mongodb://localhost:27017").use { client =>
      for
        db <- client.getDatabase("myapp")
        collection <- db.getDirectCollection[User]("users")

        users = BaseMongoRepository(collection)

        _ <- users.insertOne(User("1", "Alice", 30))
        alice <- users.findOne("1")
        adults <- users.findByFilter(Field.of[User, Int](_.age).gte(18))
      yield ()
    }

Swap mongo4s-cats for mongo4s-zio / mongo4s-kyo / mongo4s-rapid and the matching *Instances.given import to change runtime — the body of the for is identical. What does change is how you take the client, since each runtime keeps its own resource idiom: cats gives you a Resource to .use, ZIO a scoped ZIO and kyo a Scope effect (both bind inside the for and are discharged by ZIO.scoped/Scope.run), and rapid takes the body as a function — MongoClientResource.fromConnectionString(uri) { client => … }. See examples/ for one app per runtime. BsonEncoder/BsonDecoder for built-in types (String, Int, Option, List, Vector, Set, Seq, …) resolve with no import at all — no mongo4s.bson.BsonInstances.given needed unless you're summoning one directly.

Already have a model on medeia, zio-schema, or calypso? Swap derives WireCodec + getDirectCollection for derives MedeiaDocumentCodec/etc. + getCollection (and BaseMongoRepository.create[F, S, User, String](db, "users") instead of constructing it from a collection directly — E and K come from the type arguments, not from the arguments, so they have to be written out) — see BSON codecs below for all four backends.

Already have a MongoClientSettings built elsewhere (connection pool tuning, read/write concerns, TLS, credentials, …)? Use MongoClientResource.fromSettings instead of fromConnectionString. MongoClient.fromClient/fromSettings/ fromConnectionString give you the same thing unwrapped, if you'd rather own .close yourself. MongoClientResource.fromClient(client) takes the driver client over: releasing the resource closes it, so hand it a client nothing else is still using.

client.ping(timeout) asks the server for {ping: 1} and answers whether it replied in time — false rather than a failure when it did not, the timeout applied by the driver itself. For a readiness check that should not wait on the network, MongoClientResource.reachability(client, interval) pings in the background and hands back the last answer:

scala
MongoClientResource.reachability(client, 5.seconds).use { reachable =>
  reachable.flatMap(up => IO.println(s"mongo reachable: $up"))
}

MongoClientSettings sets read/write concerns for the whole client. To narrow them, MongoDatabase and MongoCollection both carry withReadConcern/withWriteConcern/withReadPreference, each returning a new handle rather than mutating the one you have:

scala
val durable = collection.withWriteConcern(WriteConcern.MAJORITY)
durable.insertOne(user)          // majority-acknowledged
collection.insertOne(other)      // still the client's default

Because the derived handle is an ordinary MongoCollection, per-operation control is just chaining: collection.withWriteConcern(w).insertOne(x). Concerns inherit database → collection, as they do in the driver, and a collection derived this way keeps the codec it was opened with — including the WireCodec that getDirectCollection registers.

withTimeout(duration) does the same for the driver's client-side operation timeout (timeoutMS): every operation through the handle — server selection, the command and reading its cursor — has that long in all, and one that runs out fails as MongoError.ExecutionTimeout. Duration.Zero means no limit; set on a database, it is inherited by the collections opened from it. The fake accepts it and ignores it, as it ignores maxTime.

scala
val bounded = collection.withTimeout(500.millis)
bounded.find(adults).all          // fails as ExecutionTimeout after half a second in all

A driver CodecRegistry is not how you plug a codec into mongo4s, though — see Codecs and the driver's registry.

For more examples see examples/src/main/scala/mongo4s/examples — a shared domain model (opaque types, enums, nested case classes) run through every runtime/codec combination (cats + medeia, ZIO + zio-bson, kyo + medeia, rapid + calypso), a repository example covering all four BaseMongoRepository construction styles against bson-direct, and a sessions/transactions + typed aggregation pipeline example on cats + medeia. Most of what this README shows is compiled there too — ReadmeSnippets.scala walks the same ground section by section, so if the API moves and these docs don't, CI fails.

Adopters

Betby

Using mongo4s? Open a PR to add your logo.

Where next

built from README.md at v6.0.0 edit this page on GitHub