"NoSQL" isn't one thing, and treating DynamoDB, Cosmos DB, and MongoDB as interchangeable is how teams end up with a data model that fights the database instead of working with it. Each of these makes a different bet about what you'll query by, how you'll scale, and what consistency guarantees cost you. Here's how we think about the choice from a .NET codebase.

Key takeaways
  • Partition (or shard) key design is the single highest-leverage decision in any of these three — get it wrong and you're looking at a data migration, not a config change.
  • DynamoDB and Cosmos DB are managed and billed around throughput; MongoDB (self-hosted or Atlas) gives you richer ad-hoc querying at the cost of more operational ownership.
  • Default consistency differs in ways that matter: DynamoDB is eventually consistent unless you opt into strongly consistent reads; Cosmos DB gives you five tunable levels; MongoDB is strongly consistent for single-document operations by default.
  • Reaching for NoSQL because it's "more modern" is how teams end up re-implementing joins in application code. Reach for it when you have a specific access-pattern or scale problem a relational store doesn't solve cleanly.

Start from the access pattern, not the data model

Relational design starts with the entities and normalizes; NoSQL design starts with the queries you'll actually run and shapes the data to answer them cheaply. If you can't list your top five access patterns before you open the SDK docs, that's the actual first step — not picking a database.

DynamoDB from .NET

DynamoDB's object persistence model maps client-side classes directly to table items via DynamoDBContext, which keeps the code close to what you'd write against EF Core.

csharp
[DynamoDBTable("Orders")]
public class OrderItem
{
    [DynamoDBHashKey("PK")]
    public string CustomerId { get; set; } = string.Empty;

    [DynamoDBRangeKey("SK")]
    public string OrderId { get; set; } = string.Empty;

    public decimal Total { get; set; }
    public string Status { get; set; } = "Draft";
}

var context = new DynamoDBContext(new AmazonDynamoDBClient());

await context.SaveAsync(new OrderItem
{
    CustomerId = "cust-482",
    OrderId = "ord-9931",
    Total = 128.50m,
    Status = "Submitted"
});

var orders = await context.QueryAsync<OrderItem>("cust-482").GetRemainingAsync();

The partition key (CustomerId here) determines how DynamoDB distributes your data across storage partitions — every query without an exact partition key match becomes a scan, which is slow and expensive at any real scale. The single-table design pattern (multiple entity types sharing one table, distinguished by key prefixes) is the DynamoDB-idiomatic way to model one-to-many and many-to-many relationships without a join; it looks strange coming from relational modeling, but it's what the database is actually optimized for.

Cosmos DB from .NET

The Microsoft.Azure.Cosmos SDK gives you a similar shape, with the partition key chosen explicitly at container creation and every request routed by it.

csharp
public class OrderDocument
{
    public string id { get; set; } = Guid.NewGuid().ToString();
    public string CustomerId { get; set; } = string.Empty;
    public decimal Total { get; set; }
    public string Status { get; set; } = "Draft";
}

var client = new CosmosClient(connectionString);
var container = client.GetContainer("shop", "orders");

await container.CreateItemAsync(
    new OrderDocument { CustomerId = "cust-482", Total = 128.50m, Status = "Submitted" },
    new PartitionKey("cust-482"));

var query = container.GetItemLinqQueryable<OrderDocument>()
    .Where(o => o.CustomerId == "cust-482");

Cosmos DB's distinguishing feature is five tunable consistency levels — Strong, Bounded Staleness, Session, Consistent Prefix, and Eventual — set at the account level but overridable per request. Session (the default) guarantees you'll always read your own writes without paying for global strong consistency, which is the right default for most application code.

MongoDB from .NET

The MongoDB.Driver package trades the partition-key rigidity of the other two for a document model with genuinely flexible ad-hoc querying and aggregation.

csharp
public class Order
{
    public ObjectId Id { get; set; }
    public string CustomerId { get; set; } = string.Empty;
    public decimal Total { get; set; }
    public string Status { get; set; } = "Draft";
}

var client = new MongoClient(connectionString);
var collection = client.GetDatabase("shop").GetCollection<Order>("orders");

await collection.InsertOneAsync(new Order { CustomerId = "cust-482", Total = 128.50m, Status = "Submitted" });

var orders = await collection.Find(o => o.CustomerId == "cust-482" && o.Total > 100)
    .SortByDescending(o => o.Total)
    .ToListAsync();

MongoDB's aggregation pipeline handles the kind of multi-condition, ad-hoc filtering and grouping that would require a scan or a secondary index in DynamoDB. That flexibility comes with sharding being something you actively manage (or pay Atlas to manage) rather than a built-in property of every table.

Consistency, side by side

DynamoDBCosmos DBMongoDB
DefaultEventually consistentSessionStrong (single document)
Strong consistencyOpt-in per read, single-regionOpt-in, costs more RUsDefault for single-doc ops
Query flexibilityKey-driven, limited scansSQL-like query languageRich aggregation pipeline
Operational modelFully managed, serverless billingFully managed, serverless or provisionedSelf-hosted or Atlas managed
Before you commitWhichever you pick, prototype your top five access patterns against real (or realistic) data volume before committing to a partition key or shard key. This is the one decision in the whole system that's genuinely expensive to reverse later.

When a relational database is still the better call

If your access patterns involve ad-hoc joins across many entities, multi-record ACID transactions, or you don't yet know your query patterns well enough to design a partition key, a relational database with good indexing will serve you better and with less operational surprise. NoSQL earns its complexity when you have a specific, known access pattern or scale requirement it solves more cleanly than a relational store — not as a default.

Need help with this in production?

astrivo helps teams design, build, and modernize systems like this. Let's talk about your context.