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.Driveris 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
✅ Correct: high-cardinality, query-aware
❌ 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 queries — RU sticker shock.