Skip to content

NoSQL & Event Stores

Key Points

  • Cosmos DB — Microsoft's globally-distributed NoSQL. Multiple APIs: NoSQL (native), MongoDB, Cassandra, Gremlin, Table. Key concepts: partition keys, RUs (request units), consistency levels, change feed.
  • MongoDB — document store. .NET driver MongoDB.Driver is mature; LINQ provider supported.
  • Marten — Postgres + JSONB as a document/event store. Best of both worlds for .NET teams already on Postgres.
  • EventStoreDB — purpose-built event store with subscriptions, projections, snapshotting.
  • Event sourcing — store events not state. CQRS-friendly; replay is the source of truth.
  • Choose based on data shape: relational + JSON columns suffice for many "NoSQL needs"; reserve dedicated NoSQL for true partitioning / global distribution / event-sourcing requirements.

Concepts (deep dive)

Cosmos DB primer

var cosmos = new CosmosClient(connStr);
var container = cosmos.GetContainer("MyDb", "Orders");

// Write
await container.CreateItemAsync(order, new PartitionKey(order.CustomerId.ToString()));

// Read by id + partition key (single-RU read)
var order = await container.ReadItemAsync<Order>(id, new PartitionKey(customerId));

// Query within a partition
var query = container.GetItemLinqQueryable<Order>(true)
    .Where(o => o.CustomerId == customerId && o.Status == "Active");
using var iter = query.ToFeedIterator();
while (iter.HasMoreResults)
    foreach (var o in await iter.ReadNextAsync()) yield return o;

Critical concepts:

  • Partition key — required; chosen at container creation. Single value per item; cannot change without rewriting. Pick high-cardinality, evenly distributed values (customer ID, tenant ID, hash). Bad keys → "hot partition" with all writes on one node.
  • RU (Request Unit) — Cosmos's unit of throughput. Each operation costs RUs (1KB read = ~1 RU; write = ~5+ RUs). Provision per second; throttle if exceeded.
  • Consistency levels — Strong, Bounded Staleness, Session (default), Consistent Prefix, Eventual. Trade-off: consistency vs latency vs cost.

Cosmos DB consistency

Level Guarantee Latency
Strong Linearizable Highest (multi-region writes block)
Bounded Staleness At most K versions / T time stale Medium
Session Read-your-own-writes Lowest
Consistent Prefix No-out-of-order reads Low
Eventual Last write wins eventually Lowest

Default Session is right for most apps. Move to Strong only when you need it (financial, distributed locks).

Cosmos DB change feed

var changeFeedProcessor = container.GetChangeFeedProcessorBuilder<Order>("name", async (changes, ct) =>
{
    foreach (var order in changes) await PublishAsync(order, ct);
})
.WithLeaseContainer(leaseContainer)
.Build();

await changeFeedProcessor.StartAsync();

Change feed is Cosmos's built-in event log — every insert/update flows through. Use for materialized views, integration with downstream systems, search indexing.

MongoDB with .NET

var client = new MongoClient(connStr);
var db = client.GetDatabase("myapp");
var orders = db.GetCollection<Order>("orders");

await orders.InsertOneAsync(order);
var found = await orders.Find(o => o.CustomerId == id).FirstOrDefaultAsync();
await orders.UpdateOneAsync(
    o => o.Id == id,
    Builders<Order>.Update.Set(o => o.Status, "Shipped"));

MongoDB.Driver provides LINQ + builder syntax. Conventions: _id is PK; embed for atomic-update access patterns.

Marten — Postgres as document store

builder.Services.AddMarten(opts =>
{
    opts.Connection(connStr);
    opts.Schema.For<Order>().Index(x => x.CustomerId);
});

using var session = store.LightweightSession();
session.Store(order);
await session.SaveChangesAsync();

var orders = await session.Query<Order>()
    .Where(o => o.CustomerId == id)
    .ToListAsync();

Marten stores documents as JSONB in Postgres with computed indexes. Pros: ACID, joins available, familiar Postgres ops. Cons: not multi-region by default (Postgres replication is your tool).

Event sourcing patterns

public abstract record OrderEvent(Guid OrderId, DateTimeOffset At);
public record OrderPlaced(Guid OrderId, DateTimeOffset At, decimal Total) : OrderEvent(OrderId, At);
public record OrderShipped(Guid OrderId, DateTimeOffset At, string TrackingNumber) : OrderEvent(OrderId, At);
public record OrderCancelled(Guid OrderId, DateTimeOffset At, string Reason) : OrderEvent(OrderId, At);

public class Order
{
    public Guid Id { get; private set; }
    public OrderStatus Status { get; private set; }
    public decimal Total { get; private set; }

    public void Apply(OrderEvent e)
    {
        switch (e)
        {
            case OrderPlaced p: Id = p.OrderId; Total = p.Total; Status = OrderStatus.Placed; break;
            case OrderShipped: Status = OrderStatus.Shipped; break;
            case OrderCancelled: Status = OrderStatus.Cancelled; break;
        }
    }
}

// Save: append events, never update state
session.Events.Append(orderId, new OrderPlaced(orderId, DateTime.UtcNow, 100m));

// Load: replay
var order = await session.Events.AggregateStreamAsync<Order>(orderId);

Marten + EventStoreDB both support this natively. State is computed by fold (aggregate) over the event stream.

Snapshotting for performance: persist current state every N events; replay only events since the snapshot.

CQRS with event sourcing

  • Write side: append events to streams.
  • Read side: project events into views (denormalized, query-optimized).
  • Replay: rebuild the read side from events at any time.

This is powerful but adds complexity. Use only when: - Audit / time-travel are core requirements. - Read patterns vastly differ from write model. - Team has experience with the pattern.

When to choose what

Need Tool
Globally distributed, multi-tenant, scale-out Cosmos DB
JSON-shaped data on Postgres Marten / native JSONB
Existing MongoDB infra MongoDB driver
Pure event-sourced system EventStoreDB or Marten Events
Mostly relational + a few JSON columns EF Core + JSON column

Code: correct vs wrong

❌ Wrong: Cosmos partition key based on row count

PartitionKey = order.Id.ToString()   // unique per item → no co-location → high RU

✅ Correct: high-cardinality, query-aware

PartitionKey = order.CustomerId.ToString()   // queries by customer co-locate

❌ Wrong: cross-partition queries on every read

var orders = container.GetItemLinqQueryable<Order>(allowSynchronousQueryExecution: false)
    .Where(o => o.Status == "Active")   // no partition key filter → cross-partition
    .ToList();
// Slow + RU-expensive

✅ Correct: scope by partition key

var query = container.GetItemLinqQueryable<Order>()
    .Where(o => o.CustomerId == customerId && o.Status == "Active");

❌ Wrong: event sourcing for everything

For typical CRUD: state-based with EF Core is simpler and sufficient. Event sourcing is for systems where audit/replay/rebuilding is core.


Design patterns for this topic

Pattern 1 — "Cosmos partition key = top dimension of query"

  • Intent: co-locate items typically queried together.

Pattern 2 — "Change feed for materialized views"

  • Intent: project Cosmos data into downstream stores (search, cache).

Pattern 3 — "Marten for Postgres-based hybrid"

  • Intent: document/event store with relational fallback.

Pattern 4 — "Snapshot in event-sourced aggregates"

  • Intent: bound replay cost.

Pattern 5 — "EF Core JSON column for occasional JSON needs"

  • Intent: don't reach for NoSQL when one column will do.

Pros & cons / trade-offs

Store Pros Cons
Cosmos DB Global, scalable, multi-API RU billing model; partition design crucial
MongoDB Mature; flexible schema Operations to manage
Marten Postgres + docs/events Postgres ops
EventStoreDB Purpose-built event store Niche tooling
EF + JSON column Familiar Limited document features

When to use / when to avoid

  • Use Cosmos DB for global / multi-tenant scale-out.
  • Use Marten if already on Postgres and want documents/events.
  • Use event sourcing when replay/audit are core.
  • Avoid NoSQL for purely relational workloads.
  • Avoid event sourcing for typical CRUD.

Interview Q&A

Q1. Cosmos DB's partition key — why does it matter? Determines item co-location across physical partitions. Wrong key → hot partition (all writes one node) or cross-partition queries (slow + RU-expensive).

Q2. What's an RU in Cosmos? Request Unit — abstract throughput unit. Operations cost RUs; you provision throughput per second.

Q3. Default consistency level in Cosmos? Session — read-your-own-writes within a client. Right for most apps.

Q4. What's change feed? Cosmos's built-in event log of inserts/updates. Subscribe via SDK; build materialized views, downstream sync.

Q5. Marten vs MongoDB? Marten = JSONB on Postgres; ACID, joins, single ops infra. MongoDB = native document store, more mature in document-specific features. Choose by infra preference.

Q6. What's event sourcing? Persist domain events instead of current state. Reconstruct state by folding events. Provides audit, replay, time-travel.

Q7. Why snapshot in event sourcing? For long event streams (years of history), full replay is slow. Snapshot current state every N events; replay only newer events.

Q8. CQRS — what is it? Command-Query Responsibility Segregation. Separate write model from read models. Often paired with event sourcing.

Q9. When NOT to use NoSQL? Workloads with strong relational semantics (joins, transactions across many entities, normalized data). Stick with EF Core / SQL.

Q10. Cosmos consistency for financial transactions? Strong — linearizable, latest-version reads. Higher latency / cost; required for correctness.


Gotchas / common mistakes

  • ⚠️ Bad partition key — hot partitions, cross-partition queries.
  • ⚠️ Default-consistency assumed Strong — Session is default.
  • ⚠️ Event sourcing without replays in mind — schema changes need migration of events.
  • ⚠️ Marten on huge JSON docs — Postgres TOAST + slow scans.
  • ⚠️ Cross-partition queriesRU sticker shock.

Further reading