VSLive! Blog

Industry Insights, Information, and Developer News

Blog archive

Trees vs. Boxes: A .NET Developer's Guide to Cosmos DB Thinking

You already think in trees. You just don't realize it.

Every day you write C#, you create objects that nest inside other objects. An Order has a list of LineItems. A Customer has a collection of Addresses. A Project has Tasks, and those tasks have Comments. It's hierarchy all the way down -- one root, branches fanning out, sub-branches fanning out from those. That's a tree. Your domain model is a living, navigable tree.

And then you save it to SQL Server, and something interesting happens. That tree gets chopped up. The Order goes into one table. The LineItems go into a different table. The Customer goes into yet another table. The branches get separated, flattened into rows, and filed into uniform, stackable boxes -- connected not by object references but by foreign key values. Label numbers printed on each row that say "this box is related to that box."

You've been doing this for so long that you probably don't even notice the translation anymore. But it's there, and it has a cost.

Two Shapes, One Problem
Let me make this concrete. Here's an order as a JSON document -- the way it looks in your C# code and the way Cosmos DB would store it:

{
  "id": "order-2587",
  "orderDate": "2026-06-15T14:30:00Z",
  "status": "Confirmed",
  "customer": {
    "name": "Joey Jo Jo Shabbadoo",
    "email": "[email protected]"
  },
  "shippingAddress": {
    "street": "123 Fake St",
    "city": "Springfield",
    "state": "IL",
    "zipCode": "62704"
  },
  "lineItems": [
    {
      "productName": "DIY Ice Cube Kit",
      "unitPrice": 12.99,
      "quantity": 3
    },
    {
      "productName": "Gourmet Dog Food for Humans",
      "unitPrice": 24.50,
      "quantity": 1
    },
    {
      "productName": "Invisible Safety Jacket",
      "unitPrice": 149.99,
      "quantity": 2
    }
  ]
}

One object. One document. You can see the whole thing -- the customer, the address, the line items -- nested right there. It's a tree.

Now here's the same order saved to SQL Server:

-- First, make sure the Customer exists
INSERT INTO Customers (Name, Email)
VALUES ('Joey Jo Jo Shabbadoo', '[email protected]');

-- Grab the auto-generated ID so we can use it as a foreign key
DECLARE @CustomerId INT = SCOPE_IDENTITY();

-- Now insert the Order (needs the CustomerId we just captured)
INSERT INTO Orders (CustomerId, OrderDate, Status)
VALUES (@CustomerId, '2026-06-15T14:30:00Z', 'Confirmed');

-- Grab that ID too -- we'll need it for everything else
DECLARE @OrderId INT = SCOPE_IDENTITY();

-- Shipping info references the Order
INSERT INTO ShippingInfo (OrderId, Street, City, State, ZipCode)
VALUES (@OrderId, '123 Fake St', 'Springfield', 'IL', '62704');

-- Each line item is a separate INSERT, each referencing the OrderId
INSERT INTO OrderItems (OrderId, ProductName, UnitPrice, Quantity)
VALUES (@OrderId, 'DIY Ice Cube Kit', 12.99, 3);

INSERT INTO OrderItems (OrderId, ProductName, UnitPrice, Quantity)
VALUES (@OrderId, 'Gourmet Dog Food for Humans', 24.50, 1);

INSERT INTO OrderItems (OrderId, ProductName, UnitPrice, Quantity)
VALUES (@OrderId, 'Invisible Safety Jacket', 149.99, 2);

Six INSERT statements across four tables. Two SCOPE_IDENTITY() calls to capture auto-generated IDs. The customer has to exist before you can create the order -- and you have to capture the customer's ID before you can write the order's INSERT. The order has to exist before you can add shipping info or line items -- and you have to capture that ID too. Each line item is a separate row in a separate table, threaded back to the order by that captured @OrderId . And this is a simple order with only three items -- imagine 50.

That's the difference between trees and boxes. The JSON is one thing with structure. The SQL is the same data chopped apart, flattened into rows, and filed into separate tables -- connected by ID values threaded through everything like label numbers.

This isn't SQL Server's fault. Relational databases are brilliant at what they're designed for: storing flat, uniform data and answering ad-hoc questions you haven't thought of yet. "Give me all orders over $500 from customers in Illinois placed in the last 30 days" -- that's a natural question in relational-land. The flatness is a feature, not a bug.

But the flatness means you're always converting. To save: chop the tree, pack it into boxes. To load: open the boxes, reassemble the tree. Hundreds of times a second, thousands of times a day. Entity Framework Core does a remarkable job managing this translation -- ORMs in general are some of the most impressive engineering in our ecosystem -- but they can't eliminate the fundamental tension. Trees and boxes are different shapes.

What If the Database Stored the Tree?
This is the mental shift that makes Cosmos DB click.

Cosmos DB is a document database. It stores JSON documents. And what is a JSON document? It's a tree. Hierarchy intact. Branches in place. That order you just saw -- the one with Joey Jo Jo's questionable shopping habits -- gets stored as-is. One document, one tree, no chopping required.

When you read it back, you get the tree back. Same shape. No reassembly. No JOINs. No foreign key choreography. You saved a tree, you got back a tree.

If you've ever worked with Cosmos DB and had that "wait... that's it?" moment -- that feeling is the impedance mismatch evaporating. A huge category of problems that consume your time and architectural energy in the relational world simply don't exist when the storage shape matches the object shape.

No navigation properties configured for lazy loading. No N+1 query surprises. No foreign key properties cluttering up your model. The tree is the document is the domain object.

The Tradeoff (Because There's Always a Tradeoff)
I'm not going to sit here and tell you Cosmos DB is "better" than SQL Server. That's not how this works. They're different shapes, optimized for different problems.

SQL Server's superpower is answering questions you haven't thought of yet. Because everything is flat and joined by keys, you can slice and dice the data any way you want. "Show me the average order value by product category by quarter for the western region" -- that's a natural relational query, even if nobody anticipated needing it when the schema was designed. The warehouse full of flat, labeled boxes is spectacularly good at ad-hoc analysis.

Cosmos DB's superpower is answering the questions you have thought of, fast and at scale. You design your documents around your access patterns. "Load this order with all its details" -- that's one read, one document, done. But "show me all line items across all orders for a specific product" -- that question might require rethinking your document boundaries, because the data isn't flat and joinable anymore. It's nested inside trees, and trees don't like being queried sideways.

You also need to think about partition keys (how Cosmos distributes your data for scale), request units (how Cosmos charges for work), and document boundaries (which things belong nested inside which other things). These aren't harder than relational design decisions -- they're just different design decisions.

The Developer Intuition Version
Here's the shortest version I can give you:

If your data is mostly read and written as a unit -- "load the order and all its stuff" -- the tree shape probably wins. You're already thinking in trees in your C# code, and Cosmos DB stores trees. The shapes match, and the whole translation layer between shapes disappears from your architecture.

If you need to slice your data 10 different ways, join across boundaries you didn't anticipate, and run ad-hoc analytical queries -- the box shape probably wins. Relational databases are designed for exactly this, and fighting against that design is painful.

A lot of real-world applications need both. That's fine. Use the right shape for the right job. The important thing is understanding that you have a choice, and that the choice is fundamentally about shapes: trees for your transactional, application-facing data, boxes for your analytical, query-everything data.

Once you see the shapes, you can't unsee them. And once you stop unconsciously converting between them, you start noticing how much of your architecture existed just to manage the conversion.

Shameless Plug
If this way of thinking about data design interests you, I wrote a whole book about it. Azure Cosmos DB for .NET Developers walks through everything from the mental model to building production applications -- document design, partition strategies, the SDK, repositories, testing patterns, security, and a complete ASP.NET Core application backed by Cosmos DB.

It's written for .NET developers who already know relational databases and want to understand when and how document storage changes the game. I think you'll dig it.

About the Author

Benjamin Day is a consultant, trainer and author specializing in software development, project management and leadership.

Posted by Benjamin Day on 06/23/2026


Keep Up-to-Date with Visual Studio Live!

Email Address*Country*
Please type the letters/numbers you see above.