# Choosing an event store

> Which event store to pick, and how MongoDB stores events and time.

There are currently two different datastores to choose from, [MongoDB](/docs/stores/choosing-an-event-store/#mongodb) and [In-Memory](/docs/stores/in-memory-event-store/).

## MongoDB

Uses MongoDB, version 4.2 or above, as the underlying datastore for the CloudEvents. All implementations use transactions to guarantee consistent writes (see [WriteCondition](/docs/layer-0/write-conditions/)). Each EventStore will automatically create a few [indexes](/docs/stores/mongodb-indexes/) on startup to allow for fast consistent writes, optimistic concurrency control and to avoid duplicated events. These indexes can also be used in queries against the EventStore (see [EventStoreQueries](/docs/layer-0/eventstore-queries/)).

There are three different MongoDB EventStore implementations to choose from:

* [Native Driver](/docs/stores/mongodb-event-stores/#eventstore-with-mongodb-native-driver)
* [Spring (Blocking)](/docs/stores/mongodb-event-stores/#eventstore-with-spring-mongotemplate-blocking)
* [Spring (Reactive)](/docs/stores/mongodb-event-stores/#eventstore-with-spring-reactivemongotemplate-reactive)

### MongoDB Schema

All MongoDB EventStore implementations try to stay as close as possible to the [CloudEvents](https://cloudevents.io/) specification even in the persistence layer. Occurrent, by default, automatically adds a custom “Occurrent extension” to each cloud event that is written to an `EventStore`. The Occurrent CloudEvent Extension consists of these attributes:

| Attribute Name  |          Type          | Description                                                                                                           |
| :-------------- | :--------------------: | :-------------------------------------------------------------------------------------------------------------------- |
| `streamid`      |         String         | An id that uniquely identifies a particular event stream. It’s used to determine which events belong to which stream. |
| `streamversion` |          Long          | The id of the stream version for a particular event. It’s used for optimistic concurrency control.                    |

A json schema describing a complete Occurrent CloudEvent, as it will be persisted to a MongoDB collection, can be found [here](https://github.com/johanhaleby/occurrent/blob/occurrent-0.34.0/cloudevents-schema-occurrent.json) (a “raw” cloud event json schema can be found [here](https://github.com/tsurdilo/cloudevents-schema-vscode/blob/master/schemas/cloudevents-schema.json) for comparison).

Note that MongoDB will automatically add an [\_id](https://docs.mongodb.com/manual/reference/method/ObjectId/) field (which is not used by Occurrent). The reason why the CloudEvent `id` attribute is not stored as `_id` in MongoDB is that the `id` of a CloudEvent is not globally unique! The combination of `id` and `source` is a globally unique CloudEvent. Note also that `_id` will *not* be included when the `CloudEvent` is read from an `EventStore`.

Here’s an example of what you can expect to see in the “events” collection when storing events in an `EventStore` backed by MongoDB (given that `TimeRepresentation` is set to `DATE`):

```javascript
{
  "_id" : ObjectId("5f4112a348b8da5305e41f57"),
  "specversion" : "1.0",
  "id" : "bdb8481f-9e8e-443b-80a4-5ef787f0f227",
  "source" : "urn:occurrent:domain:numberguessinggame",
  "type" : "NumberGuessingGameWasStarted",
  "subject" : "a1fc6ba1-7cd4-45cf-8dcc-b357fe23956d",
  "time" : ISODate("2020-08-22T14:42:11.712Z"),
  "data" : {
    "secretNumberToGuess" : 8,
    "startedBy" : "003ab97b-df79-4bf1-8c0c-08a5dd3701cf",
    "maxNumberOfGuesses" : 5
  },
  "streamid" : "a1fc6ba1-7cd4-45cf-8dcc-b357fe23956d",
  "streamversion" : NumberLong(1)
}
{
  "_id" : ObjectId("5f4112a548b8da5305e41f58"),
  "specversion" : "1.0",
  "id" : "c1bfc3a5-1716-43ae-88a6-297189b1b5c7",
  "source" : "urn:occurrent:domain:numberguessinggame",
  "type" : "PlayerGuessedANumberThatWasTooSmall",
  "subject" : "a1fc6ba1-7cd4-45cf-8dcc-b357fe23956d",
  "time" : ISODate("2020-08-22T14:42:13.336Z"),
  "data" : {
    "guessedNumber" : 1,
    "playerId" : "003ab97b-df79-4bf1-8c0c-08a5dd3701cf"
  },
  "streamid" : "a1fc6ba1-7cd4-45cf-8dcc-b357fe23956d",
  "streamversion" : NumberLong(2)
}
```

### MongoDB Time Representation

The CloudEvents specification says that the [time attribute](https://github.com/cloudevents/spec/blob/v1.0.2/cloudevents/spec.md#time), if present, must adhere to the [RFC 3339 specification](https://tools.ietf.org/html/rfc3339). To accommodate this in MongoDB, the `time` attribute must be persisted as a `String`. That by itself is not a problem. It only becomes one when you make time-based queries on the events persisted to a MongoDB-backed `EventStore` (using the [EventStoreQueries](/docs/layer-0/eventstore-queries/) interface), because MongoDB then compares those strings character by character rather than as points in time. What we *would* like to do is to persist the `time` attribute as a `Date` in MongoDB, but MongoDB internally represents a Date with only millisecond resolution (see [here](https://docs.mongodb.com/manual/reference/method/Date/#behavior)) and then the CloudEvent cannot be compliant with the RFC 3339 specification in *all* circumstances.

Occurrent makes the string comparison behave for the common case by writing every `time` in one canonical shape, always with seconds and always with nine fractional digits, for example `2026-07-28T12:00:00.000000000Z`. Because every stored value then has the same width, the character-by-character comparison follows chronological order. It has two limits, and both are covered under [time queries with RFC_3339_STRING](/docs/stores/choosing-an-event-store/#time-queries-with-rfc_3339_string) below. Rationale in [ADR 79](https://github.com/johanhaleby/occurrent/blob/main/doc/architecture/decisions/0079-canonical-fixed-width-time-for-rfc3339-storage.md).

Because of the reasons described above, users of a MongoDB-backed EventStore implementation, must decide how the `time` attribute is to be represented in MongoDB when instantiating an EventStore implementation. This is done by passing a value from the `org.occurrent.mongodb.timerepresentation.TimeRepresentation` enum to an `EventStoreConfig` object that is then passed to the EventStore implementation. `TimeRepresentation` has these values:

| Value                 | Description                                                                                                                                                                                                                                                                                                                                                                                                                        |
| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RFC_3339_STRING`     | Persist the time attribute as an RFC 3339 string. This string is able to represent both nanoseconds and a timezone so this is recommended for apps that need to store this information or if you are uncertain of whether this is required in the future. Time-based queries work, with the two caveats described in [time queries with RFC_3339_STRING](/docs/stores/choosing-an-event-store/#time-queries-with-rfc_3339_string). |
| `DATE`                | Persist the time attribute as a MongoDB [Date](https://docs.mongodb.com/manual/reference/method/Date/#behavior). The benefit of using this approach is that you can do range queries etc on the “time” field on the cloud event. This can be really useful for certain types on analytics or projections (such as show the 10 latest number of started games) without writing any custom code.                                     |

Note that if you choose to go with `RFC_3339_STRING` you always have the option of adding a custom attribute, named for example “date”, in which you represent the “time” attribute as a `Date` when writing the events to an `EventStore`. This way you have the ability to use the “time” attribute to reconstruct the CloudEvent time attribute exactly as well as the ability to do *custom* time-queries on the “date” attribute.

#### Time queries with RFC_3339_STRING

Time-based queries through the [EventStoreQueries](/docs/layer-0/eventstore-queries/) interface work under `RFC_3339_STRING`, because Occurrent stores every `time` in the one canonical shape described above and renders a query’s instant the same way. An exact match finds the event written at that instant, and a range condition orders correctly.

There are two limits.

Ordering relies on every stored value sharing the same UTC offset. `2026-07-28T14:00:00.000000000+02:00` and `2026-07-28T12:00:00.000000000Z` are the same instant, but as strings they do not sort the same way, so a collection whose events carry mixed offsets can order them wrongly. Occurrent deliberately does not normalise the offset away, because keeping the timezone is the reason to pick `RFC_3339_STRING` in the first place. If your events span several offsets and you need range queries over them, use `DATE`, or keep a separate custom attribute as described above.

Events written by an older version of Occurrent were stored in a shorter shape, one that omitted trailing zeroes. An exact filter, `Filter.time(instant)` built from `eq`, `in`, or `ne`, still matches such an event, because Occurrent checks it against both the canonical shape and the legacy shape a value could have been written with. A range filter (`lt`, `gt`, `lte`, `gte`) is not extended this way, since a legacy and a canonical value do not reliably sort against each other, so a range boundary that lands on a pre-upgrade event can still miss it. Newly written events are unaffected either way.

If the range-filter gap matters to you, rewrite the `time` field of existing documents into one fixed shape once. Run this against your event collection, with the server on MongoDB 4.2 or later so `$dateToString` is available in an aggregation pipeline update, and take a backup first:

```js
db.events.updateMany(
  {
    $and: [
      { time: { $type: "string" } },
      { time: { $not: /\.\d{4,}/ } }
    ]
  },
  [
    {
      $set: {
        time: {
          $concat: [
            {
              $dateToString: {
                date: { $dateFromString: { dateString: "$time" } },
                format: "%Y-%m-%dT%H:%M:%S.%L"
              }
            },
            "000000Z"
          ]
        }
      }
    }
  ]
)
```

The match stage skips a value whose fractional part already has more than three digits. `$dateFromString` parses into a BSON date, which only holds milliseconds, so touching a value that already carries more precision than that would throw the extra digits away. That protects two kinds of document from the same mistake. One is an event whose original timestamp genuinely had sub-millisecond precision. The other is an event that 0.32.0 already wrote in the canonical nine-digit shape, which this script must not touch a second time either.

For an event the script does rewrite, it pads the three digits `$dateToString` produces out to nine with zeroes, so the result has the canonical fixed width and sorts correctly against the values 0.32.0 writes for new events. Those zeroes are filler, not real digits, so a rewritten value equals the canonical shape a filter checks only when the original event had no sub-millisecond precision to lose in the first place.

An event the script skips keeps the shape it already had. An equality filter still matches it exactly as it did before you ran anything, through the legacy shape described above, because its stored value never changed. What stays open for it is the range-query gap. If you want that fixed too, rewrite it in application code instead, reading it with `OffsetDateTime.parse` and writing it back with nine real fractional digits, since only that route can supply digits `$dateFromString` would otherwise discard.

**Important**: There’s yet another option! If you don’t need nanotime precision (i.e you’re fine with millisecond precision) *and* you’re OK with always representing the “time” attribute in UTC, then you can use `TimeRepresentation.DATE` without loss of precision! This is why, if `DATE` is configured for the `EventStore`, Occurrent will refuse to store a `CloudEvent` that specifies nanotime and is not defined in UTC (so that there won’t be any surprises). I.e. using `DATE` and then doing this will throw an `IllegalArgumentException`:

```java
var cloudEvent = new CloudEventBuilder().time(OffsetDateTime.now()). .. .build();


// Will throw exception since OffsetDateTime.now() will include nanoseconds by default in Java 9+
eventStore.write(List.of(cloudEvent));
```

Instead, you need to remove nano seconds do like this explicitly:

```java
// Remove millis and make sure to use UTC as timezone
var now = OffsetDateTime.now().truncatedTo(ChronoUnit.MILLIS).withOffsetSameInstant(ZoneOffset.UTC);
var cloudEvent = new CloudEventBuilder().time(now). .. .build();


// Now you can write the cloud event
```

For more thoughts on this, refer to the [architecture decision record](https://github.com/johanhaleby/occurrent/blob/main/doc/architecture/decisions/0004-mongodb-datetime-representation.md) on time representation in MongoDB.