---
title: Map MongoDB concepts
description: Register concrete scalar pairs and identifiers so a ConceptAs value round-trips through MongoDB.
---


## Map MongoDB concepts explicitly in the application

MongoDB concept storage is also an **application-owned recipe**, not Arc autodiscovery. Register
concrete, stateless `@WritingConverter`/`@ReadingConverter` pairs for each concept and scalar in
one authoritative `MongoCustomConversions.create` configuration. A bare Spring `Converter` bean
is not this registration. Configure the application's mapping context and converter before
creating templates, repositories, or tenant certificates; Arc must not mutate an application-owned
custom `MongoConverter` during lookup.

### Register concrete scalar pairs

For a concept such as the `TextValue` shown in [Map JPA concepts](/arc/backend/kotlin/guides/spring-data/concepts-jpa/), the Kotlin pair is:

```kotlin
@WritingConverter
class TextWrite : Converter<TextValue, String> {
    override fun convert(source: TextValue): String = source.value()
}

@ReadingConverter
class TextRead : Converter<String, TextValue> {
    override fun convert(source: String): TextValue = TextValue(source)
}
```

The ordinary Java equivalent constructs the record explicitly:

```java
@WritingConverter
public class TextWrite implements Converter<TextValue, String> {
    @Override
    public String convert(TextValue source) { return source.value(); }
}

@ReadingConverter
public class TextRead implements Converter<String, TextValue> {
    @Override
    public TextValue convert(String source) { return new TextValue(source); }
}
```

Here `Converter` means Spring's `org.springframework.core.convert.converter.Converter`, not the
JPA annotation. The executable recipes provide equivalent concrete UUID-to-UUID and Long-to-Long
pairs named `UuidWrite`/`UuidRead` and `LongWrite`/`LongRead`. Do not replace them with an erased
`Converter<ConceptAs<?>, ?>` or infer constructors reflectively. Install all six together:

```kotlin
val conversions = MongoCustomConversions.create { adapter ->
    adapter.registerConverter(UuidWrite())
    adapter.registerConverter(UuidRead())
    adapter.registerConverter(TextWrite())
    adapter.registerConverter(TextRead())
    adapter.registerConverter(LongWrite())
    adapter.registerConverter(LongRead())
}
```

```java
var conversions = MongoCustomConversions.create(adapter -> {
    adapter.registerConverter(new UuidWrite());
    adapter.registerConverter(new UuidRead());
    adapter.registerConverter(new TextWrite());
    adapter.registerConverter(new TextRead());
    adapter.registerConverter(new LongWrite());
    adapter.registerConverter(new LongRead());
});
```

For programmatically owned templates, use that same configuration in both places, in this order
(the complete `RecipeMongoStore` fixture also owns and closes its client):

```java
var mappingContext = new MongoMappingContext();
mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder());
mappingContext.setInitialEntitySet(Set.of(UuidRow.class, TextRow.class, LongRow.class));
mappingContext.afterPropertiesSet();

var factory = new SimpleMongoClientDatabaseFactory(client, database);
var converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), mappingContext);
converter.setCustomConversions(conversions);
converter.afterPropertiesSet();
var template = new MongoTemplate(factory, converter);
var repository = new MongoRepositoryFactory(template).getRepository(TextRows.class);
```

Configure the client explicitly with
`MongoClientSettings.builder().uuidRepresentation(UuidRepresentation.STANDARD)` before building
it. The tests read raw `BsonDocument` values and assert UUID binary **subtype 4**, BSON string,
and BSON int64 for IDs and ordinary fields, not nested wrappers or numeric strings.

### Choose identifier and null mappings deliberately

The recipes use Spring Data `@Id` for UUID and Long concept identifiers, with the concrete pairs
above. For the String concept identifier, use `@MongoId(FieldType.STRING)` explicitly: the tested
24-hex value `507f1f77bcf86cd799439011` stays a BSON string instead of becoming an `ObjectId`.
Do not substitute an unqualified `@MongoId` for these mappings; its implicit target is not the
same conversion policy. Declare repository IDs as the concrete concept type, for example
`MongoRepository<TextRow, TextValue>`.

Kotlin documents use constructor-bound concept IDs and mutable ordinary fields. Ordinary Java
documents use no-argument construction and field hydration, with immutable concept records as
their values; this avoids relying on Java constructor parameter-name compiler metadata. Do not
change an identifier after saving a document.

Spring bypasses these scalar converters for null. Apply `@Field(write = Field.Write.ALWAYS)`
(`@field:Field(write = Field.Write.ALWAYS)` in Kotlin) when a nullable concept field must be
written as explicit BSON null. Without it, the tested null field is absent. Both read as null;
the tests separately remove an explicitly written field and verify absent-field hydration.
Non-null blank text fails in the concept constructor. A stored String in the Long field fails
with `ConverterNotFoundException` rather than becoming a default Long concept; the configured
reading pair accepts Long, not arbitrary malformed storage types.

MongoDB documents are not JPA managed entities: changing a loaded field alone does not persist
it. Call repository `save` explicitly or use a mapped `MongoTemplate.updateFirst` with a
concept-valued query and update. Verify with another read through a fresh template/converter.

### MongoDB recipe evidence and limits

`Integrations/SpringDataMongo` tests `io.cratis.arc.persistence.recipes.mongodb.MongoConceptStorageTests`
and `JavaMongoConceptStorageTests` execute configured repositories, concept `findById` and equality
predicates, fresh class reconstruction, explicit save/update, distinct null/absent BSON shapes,
malformed-value failures, and exact signed Long boundaries including values beyond JavaScript's
exact integer range. The missing-registration control stores a nested concept document instead
of a scalar; adding the application registration produces the asserted scalar. This is an
application omission control, not an existing Arc product bug.

Each language stores the same UUID, String, and Long concept keys with different values in two
databases. Templates are fully configured before issuing `TenantMongoOperations` certificates.
The existing contextual resolver rejects unknown tenants, mismatched certificates, missing/blank
tenants, scalar keys, and a different concept key class. Exact resolver-call and driver `find`
command counts prove no retry or fallback database read. Mongo certificates certify `tenantId`,
not JPA's tenant namespace; no Mongo namespace-isolation claim follows. Repository injection
alone remains ordinary Spring dependency injection, not tenant routing.

This evidence uses Spring Data MongoDB 5.1.1 and Mongo Java driver 5.8.1 against the existing
**mongo-java-server 1.47.0 `MemoryBackend` emulator**, not a real MongoDB server, Testcontainers,
or an external database. It does not certify production MongoDB, other providers or BSON types,
change streams, replica sets, transactions, optimistic locking, or Arc .NET parity. No provider
dependency or Arc API is added by the recipes.

```shell
./gradlew :Integrations:SpringDataMongo:test --tests '*MongoConceptStorageTests'
```
