# The bugs that don't fail

> 294 tests, 71 specification scenarios, and four bugs that only a real mongod would show — because none of them threw anything.

Page: https://kotlin.website/blog/bugs-that-dont-fail · Published: 2026-08-07

[mongkn](https://github.com/youndie/mongkn) is a MongoDB driver for Kotlin/Native. By the
time I moved a real service onto it, it had 294 tests and passed 71 scenarios from
MongoDB's own specifications.

Four bugs were waiting anyway. Not one of them threw. Every one returned a plausible
answer that happened to be wrong.

## The most expensive four lines

The query was ordinary:

```kotlin
collection.find { Landing::shopId eq shopId }
```

`shopId` is a `String` in Kotlin and an `ObjectId` in the database — a custom serializer
converts it on the way in. But the filter builder encoded by the value's **runtime type**,
not by the field's serializer. So the filter carried a `BsonString` and the stored field
was a `BsonObjectId`.

Mongo compared them, found nothing, and said so. Empty result, no error, no warning. The
query was valid; it just asked about a field that never has that type.

The fix is to encode a filter value through the serializer of the field it is compared
against, which needs `childSerializers()` on the generated serializer. Four lines. Days to
find, because nothing anywhere was red.

## Three more of the same shape

- **A missing `_id` mapping.** Without `@SerialName("_id")` the class writes an ordinary
  `id` field and Mongo generates its own `_id` beside it. Two identifiers, one document,
  and lookups by the one you care about quietly miss.
- **A sparse index that indexed everything.** An explicit `null` is written as `BsonNull`,
  which is a present value. Sparse means "skip documents without the key", and every
  document had the key. `@EncodeDefault(NEVER)` is what makes absent mean absent.
- **A TTL index that stopped deleting.** Timestamps stored as ISO strings rather than BSON
  dates. The index exists, it is valid, and it never expires a thing.

## The defence that was already there

The library already refused one class of mistake. A property reference in a filter is
checked against the class descriptor, so a renamed field fails immediately with a message
that says which one. That check works — nobody had ever been bitten by it, which is the
point.

So the fix was not to invent a new mechanism. It was to extend the one that already
earned its place: validate the **value** the way the **field** was already validated.

A rule went into the test docs at the same time:

> Native storage layers are verified by integration tests against a real `mongod`, not by
> unit tests. Everything here that could break broke silently, and only a real server
> showed it.

## A ladder is an upper bound, not a promise

While measuring, I built a five-level performance ladder to see what each layer costs. The
coroutine channel came out at 0.22 µs per document. Batching 64 documents across it was
predicted to save that 0.22 µs and actually saved 0.47 µs on macOS and 0.30 on Linux — the
extra came from context switches that disappeared with the crossings.

Then a decoding optimisation predicted to save 0.43 µs delivered **4 %** on the real read
path, despite being 37 % faster in isolation.

That is the lesson, and it generalises past this project:

> The cost of a layer in a ladder is an upper bound on the win, not the win. A layer that
> is not on the critical path gives you nothing when you delete it.

## The baseline that moved

A bare C loop — the floor, the thing that cannot get faster — measured 6667 µs in one run
and 10710 µs in another on the same machine. A 1.5× swing in the control.

Comparing numbers across runs was meaningless, and I had been doing exactly that.
Interleaving the rounds instead of finishing one level before starting the next made the
results reproducible, because every level then absorbed the same machine noise.

## The check that was not running

A test had been failing on one platform for weeks. It was known, it was tolerated — and
because it failed, the ABI validation task never ran. The public API drifted for weeks
behind a red light everyone had learned to read as normal.

Two changes: the reference dump moved to the platform that actually ships (`linuxX64`),
and the red test was closed by asserting a weaker property that holds on both branches.

## What I would tell myself a year earlier

1. Look for the **shape** — a wrong answer with no exception — before it costs you data.
2. Extend a defence that already works rather than adding a new kind.
3. Move a real service onto it early. Specification scenarios do not cover the four bugs
   above, because none of them are protocol violations.
4. Verify measurements under the conditions you will actually run in.

---

Source: [github.com/youndie/mongkn](https://github.com/youndie/mongkn)
