How We Got Here: The Hidden Tax of Relational Development

March 22, 2026
How We Got Here: The Hidden Tax of Relational Development

This is Chapter 1 of Azure Cosmos DB for .NET Developers, a book I'm writing and publishing here on the blog. You can follow along as it takes shape.

Before we can talk about Cosmos DB, we need to talk about the world we're coming from. Not because there's anything wrong with it — relational databases are genuinely great technology — but because there's a cost to the relational model that most application developers have been paying for so long they've stopped noticing it. It's like a noise in your house that you only hear when someone visits and says "what's that humming sound?"

This chapter is about making the humming audible. Once you can hear it, the document model isn't just an alternative — it's an answer to a question you didn't know you were asking.

Why Relational Databases Exist

Here's a piece of context that changes how you think about database design: relational databases are an engineering response to expensive storage.

When Edgar Codd published his relational model in 1970, disk storage cost roughly $700,000 per gigabyte (in 2026 dollars). Storing the same piece of data twice wasn't just wasteful — it was genuinely expensive. Normalization — the practice of splitting data into separate tables and eliminating redundancy — wasn't an aesthetic preference. It was an economic necessity. You literally couldn't afford to store a customer's name in both the Customers table and the Orders table.

That economic pressure produced an elegant system. Flat tables. Foreign keys. JOINs to reassemble related data on the fly. Every piece of information stored exactly once. Need the customer's name on an order? Don't store it — JOIN to the Customers table and look it up. A side benefit of this economic constraint is that this design ensures that the data is always consistent because there's only one copy of it.

This is genuinely brilliant engineering for the problem it was solving. And it created a model that's optimized for something most people don't think about: asking questions you haven't thought of yet. Because everything is stored separately and connected by keys, you can slice and recombine the data any way you want. "Show me all orders from Texas customers placed in Q3 that included products in the electronics category with an average item price above $50." Relational databases eat that query for breakfast. You couldn't ask that question when you designed the schema, but because the data is normalized and connected by keys, you can ask it now. That's a genuine superpower.

But that superpower comes with a structural consequence that's been shaping our code for decades: the data in the database doesn't look like the data in your application. Actually, let me put that a little more precisely: the relational data isn't structured in the same way as the data in your object-oriented application.

How Objects Work

When you're writing C#, you're thinking in terms of objects. An Order has a Customer. That customer has Addresses. You navigate the graph: order.Customer.Addresses[0].City. You're walking through relationships like paths through a tree.

Objects have properties that feel natural because they mirror how we think about things:

Identity. This Order object IS this order. Two Order instances with identical data are still two different objects. You test that with reference equality.

Behavior. Objects do things. order.CalculateTotal(). customer.ValidateAddress(). The logic lives on (or near) the objects it belongs to.

Navigation. Relationships are direct references. The Customer is RIGHT THERE inside the Order. You just follow the property.

Hierarchy. Objects naturally form trees. An Order contains LineItems. Each LineItem references a Product. The structure is nested, not flat.

This maps well to how we describe business concepts. A customer HAS addresses. An order CONTAINS line items. When your product owner describes the domain, they describe it in terms that map to objects, not tables.

Your domain model — the objects that represent your business — is a living tree. It has shape. It has structure. It branches differently depending on what the business needs. No two domain models look the same because no two businesses are the same. The tree is organic. It's alive.

How Relational Databases Work

Now let's look at the other side.

In the relational world, everything is stored in boxes. Flat, uniform, stackable boxes. A relational database stores data in tables — flat rows, all the same shape. Every customer is a row in the Customers box. Every order is a row in the Orders box. They're not nested inside each other. They sit in separate boxes, connected by label numbers printed on each row.

The properties of relational data are almost the opposite of objects:

No identity, just keys. A row doesn't "know" it's itself. It has a primary key — a label number that identifies it. Two rows with the same key are the same row by definition.

No behavior. Boxes don't do things. They store data and that's it. (Yes, you can put logic in stored procedures, but the boxes themselves are inert.)

No navigation. Want the customer for an order? You don't follow a reference. You JOIN. You ask the database to match rows from the Orders box with rows from the Customers box by comparing their label numbers. That's a fundamentally different operation from order.Customer on an object.

Sets, not trees. You're always working with sets of rows. "Give me all orders where the total exceeds $500" — that's a natural question in relational-land. The flatness makes it easy.

Think of a relational database as a warehouse. Rows and rows of uniform, labeled, stackable boxes. Perfectly organized. Easy to search. You can find anything by its label. You can combine boxes from different shelves to answer questions nobody anticipated when the warehouse was built. It's a brilliant system for storage and retrieval.

But it's nothing like a tree.

Both of these models are good at what they're designed for. Trees are great for expressing business logic with hierarchy and behavior. Boxes are great for storing data efficiently and answering ad-hoc questions. The problem is that your application needs both.

The Impedance Mismatch: Trees vs. Boxes

So here's the situation. You've got a beautiful, living domain model in your code — nested, navigable, full of behavior. And you've got a beautiful, organized warehouse in your database — flat, joinable, flexible. And you need them to talk to each other.

But those two things have completely different shapes. The database is always a collection of boxes — flat, uniform, stackable. And the domain model is always shaped like a tree — one root, branches fanning out, sub-branches fanning out from those. Order is the root. LineItems is a branch. ShippingAddress is a branch. Customer is a branch with its own Addresses sub-branch.

The problem — the whole problem, really — is that you're constantly converting one living tree into a bunch of flat boxes, and a bunch of flat boxes back into one living tree.

To save, you chop the tree down and pack the pieces into boxes. You take the Order object — this single, living tree with all its branches — and you take it apart. The order header goes into a row in the Orders box. Each line item gets packed into the OrderItems box. The shipping address gets packed into the ShippingInfo box. Label numbers (foreign keys) stitch them together so the warehouse knows which boxes belong to which other boxes. But the tree is gone. It's been pulped, flattened, and packed into storage — turned into boxes.

To read, you open the boxes and try to reassemble the tree. You pull out the rows from the Orders box, the OrderItems box, the ShippingInfo box. You match up their label numbers. And then you try to rebuild the tree — nest the line items inside the order, attach the shipping address, reconstruct the living object graph. Every single time.

And here's the thing that makes this metaphor more than a metaphor: cardboard boxes are made from dead trees. That's not just a coincidence — it's exactly what's happening in your code. You take a living, structured, meaningful domain object and you kill its structure to make it fit into flat, uniform storage. The tree had shape. It had hierarchy. It had meaning in its branching. The boxes are flat, generic, interchangeable.

And then when you try to rebuild the tree from the boxes? You're doing something that doesn't quite work in the real world. You can't bring a tree back to life from cardboard. You can glue the pieces back into roughly the right shape, but something is always at risk of being lost. A branch in the wrong place. A piece that didn't survive the packing. A label that got smudged. Every adapter bug you've ever seen is a box that got mislabeled, or a branch that ended up packed in the wrong box, or a piece that quietly disappeared between the chopping and the reassembly.

This is the object-relational impedance mismatch, and it's been frustrating developers for thirty-plus years. It's not because the tools are bad. It's because trees and boxes are fundamentally different shapes. You've got to do a lot of fiddly, essential, tedious work to convert between shapes. If miss something or put something in the wrong place, you could potentially completely change the meaning.

classDiagram
    direction LR

    namespace The_Living_Tree {
        class Order_Object {
            +Customer Customer
            +List~LineItem~ LineItems
            +ShippingAddress Address
            +CalculateTotal() decimal
            +Cancel() void
        }
        class LineItem_Object {
            +string ProductName
            +decimal UnitPrice
            +int Quantity
            +GetSubtotal() decimal
        }
    }

    namespace The_Boxes {
        class Orders_Box {
            +int OrderId PK
            +int CustomerId FK
            +DateTime OrderDate
            +string Status
        }
        class OrderItems_Box {
            +int OrderItemId PK
            +int OrderId FK
            +int ProductId FK
            +decimal UnitPrice
            +int Quantity
        }
        class ShippingInfo_Box {
            +int ShippingInfoId PK
            +int OrderId FK
            +string Street
            +string City
            +string State
        }
    }

    Order_Object *-- LineItem_Object : branch

    Orders_Box "1" --> "*" OrderItems_Box : FK OrderId
    Orders_Box "1" --> "1" ShippingInfo_Box : FK OrderId

    Order_Object ..> Orders_Box : chop tree → pack boxes
    LineItem_Object ..> OrderItems_Box : chop tree → pack boxes

On the left: one living tree. Order is the root, LineItem is a branch. It's a single, coherent thing. On the right: three separate boxes, connected by label numbers. To save, you chop the tree and pack the pieces into those boxes. To load, you open the boxes and reassemble the tree. Every time.

This isn't a flaw in Entity Framework, SQL Server, or your code. Trees and boxes are just different shapes. The impedance mismatch isn't a bug — it's just what happens when you try to store something alive in something that's flat.

BTW, this translation logic is pretty much exactly what EF Core (and other ORMs like NHibernate) are trying to solve. Object-relational mappers (ORM) have a brutally complex problem to solve and they do a surprisingly good job doing it. But it's fundamentally hard and the conversions aren't always clean and elegant.

Your EF Core Entities Are Not Your Domain Objects

This is the point that trips up a lot of developers. And by "trips up" I mean "completely ruins the technical lives of" developers. It sounds like unnecessary ceremony. "Why can't I just use my EF entity classes as my domain model? Why do I need two representations of the same thing?"

Because they aren't the same thing. They have different jobs — and more importantly, they have different shapes. They have different reasons and motivations to change, too — and they don't necessarily even change at the same time.

It probably won't surprise you to find out that I have real preferences for how I want to use EF Core. EF Core is great and I use it all the time. It can do amazing things and has SO MANY features. But just because you can use all those features doesn't mean you should. There are performance concerns and maintainability concerns and understandability concerns and on and on and on.

My preference and recommendation is that you keep your EF Core mappings simple.

An EF Core entity class is shaped like a box. It has an int Id because the box needs a label number. It has virtual ICollection<OrderItem> OrderItems because EF Core needs navigation properties for lazy loading and JOIN generation. It has foreign key properties, timestamp columns, attributes or fluent configuration that describe the box structure. It's a flat, uniform row in a flat, uniform table — a packing slip for the warehouse.

A domain model class is shaped like a tree. It has the properties and methods that express business rules. Order.CalculateTotal(). Order.CanBeCancelled(). Customer.GetDefaultShippingAddress(). It nests things naturally — the order contains its line items, the customer contains its addresses. It doesn't care about foreign keys or navigation properties or column mappings. It's the living structure your code actually thinks in.

When you try to make one class serve as both the box and the tree, it does both jobs poorly:

Your tree gets polluted with box concerns. That virtual keyword on your navigation properties isn't a tree concept — it's there for EF Core's change tracker. That int CustomerId foreign key property isn't something your tree cares about — it's a label number that tells the warehouse how boxes relate to each other.

Your tree logic has to work around box limitations. You want to model something with value objects or complex hierarchies, but EF Core can't pack it cleanly into boxes. So you compromise the shape of your tree to fit the shape of the boxes. Now the warehouse is dictating how your tree grows.

Your tree becomes coupled to your boxes. Change a column name, and your tree breaks. Add an index, and your box needs a configuration change. The tree and the boxes should be able to evolve independently. When they're the same classes, they can't.

(That problem gets worse and weirder if you try to send your EF Core entities out through your Web API service endpoints...but that's a problem for another day.)

Doing It Right: Hiding EF Core from Your Application

So if your boxes and your trees are separate things, how do you wire it all together? Here's how I structure my applications, and it's a pattern I'm somewhat obsessive about: EF Core and its boxes should be invisible to every layer except the one that directly talks to the database.

The service layer shouldn't know EF Core exists. The presentation layer definitely shouldn't know EF Core exists. The domain model shouldn't reference a single EF namespace. EF Core is an implementation detail of data access, and implementation details should be hidden behind interfaces.

Here's what the layer architecture looks like:

classDiagram
    direction TB

    namespace Presentation {
        class Controllers {
            +ViewModels and DTOs
            +API Endpoints
            +Razor Pages
        }
    }

    namespace Service_Layer {
        class Services {
            +Business Logic
            +Orchestration
            +Validation
            +Calls Repository Interfaces
            +Works with Trees ONLY
        }
    }

    namespace Domain {
        class DomainModels {
            +Business Rules
            +Behavior Methods
            +Calculated Properties
            +No EF Core References
            +Living trees
        }
    }

    namespace Data_Access {
        class RepositoryInterfaces {
            +Accept Trees
            +Return Trees
            +No EF Core Types Exposed
        }
        class RepositoryImplementations {
            +EF Core DbContext
            +LINQ Queries
            +Uses Adapters Internally
            +Chops trees into boxes
            +Reassembles boxes into trees
        }
        class Adapters {
            +Tree → Boxes for saves
            +Boxes → Tree for reads
            +Null Handling
            +Collection Reconciliation
        }
        class EntityClasses {
            +Shaped like boxes
            +Foreign Keys
            +Navigation Properties
            +EF Core Concerns ONLY
        }
    }

    namespace Database {
        class SQLServer {
            +A warehouse full of boxes
        }
    }

    Controllers --> Services : ViewModels → Trees
    Services --> RepositoryInterfaces : Trees in and out
    RepositoryInterfaces <|.. RepositoryImplementations : implements
    RepositoryImplementations --> Adapters : chop / reassemble
    RepositoryImplementations --> EntityClasses : EF Core CRUD
    Adapters --> DomainModels : knows about trees
    Adapters --> EntityClasses : knows about boxes
    EntityClasses --> SQLServer : EF Core generates SQL

The critical design decisions here:

Repository interfaces accept and return trees. Not boxes. The service layer calls IOrderRepository.GetById(orderId) and gets back an Order tree — a living domain object with all its branches. It calls IOrderRepository.Save(order) and passes in an Order tree. The service layer never sees a box. It doesn't even know that the boxes exist.

Repository implementations do the chopping and reassembly internally. The concrete OrderRepository takes an adapter and a DbContext via dependency injection. When the service layer calls Save(order), the repository uses the adapter to chop the Order tree into OrderEntity boxes, then ships those boxes to the warehouse via EF Core. When it loads data, it goes the other direction — pulls OrderEntity boxes from the warehouse, uses the adapter to reassemble them into an Order tree, returns the living tree.

Adapters are the lumberjack and the arborist. They chop trees into boxes for storage. They reassemble boxes back into trees for use. The adapter class handles the bidirectional conversion — property mapping, null handling, collection reconciliation, type conversion. The adapter is injected into the repository, and nothing else in the system knows it exists.

Boxes are completely hidden. The entity classes live in the data access layer and never escape. No service, no controller, no domain model ever references them. If the warehouse changes — if a box gets a new column, or a column gets renamed, or a table gets restructured — only the boxes and the adapter need to change. Everything else is insulated.

Here's what that looks like in a class diagram, using a Project/ProjectClass example similar to what I use in my own applications:

classDiagram
    direction LR

    namespace Domain_Trees {
        class Project {
            +string Name
            +string ProjectType
            +IList~ProjectClass~ Classes
            +IList~ProjectLookupValue~ LookupValues
            +HasChanges() bool
            +AcceptChanges() void
        }
        class ProjectClass {
            +string ClassName
            +string ClassFriendlyName
            +IList~ClassProperty~ Properties
            +GetAttributeValue(key) string
            +SetAttributeValue(key, val) void
        }
    }

    namespace The_Adapter {
        class ProjectAdapter {
            +Adapt(from: Project, to: ProjectEntity) void
            +Adapt(from: ProjectEntity, to: Project) void
        }
    }

    namespace EF_Boxes_Hidden {
        class ProjectEntity {
            +int Id
            +string Name
            +string ProjectType
            +int OwnerId
            +string OwnerType
            +virtual ICollection~ProjectClassEntity~ Classes
            +string Status
            +string CreatedBy
            +DateTime CreatedDate
            +byte[] Timestamp
        }
        class ProjectClassEntity {
            +int Id
            +int ProjectId FK
            +virtual ProjectEntity Project
            +string ClassName
            +virtual ICollection~ClassPropertyEntity~ Properties
            +string Status
            +byte[] Timestamp
        }
    }

    namespace Repository {
        class IProjectRepository {
            &lt;&lt;interface&gt;&gt;
            +GetById(id) Project
            +Save(project: Project) void
            +DeleteById(id) void
            +Search(args) IList~Project~
        }
        class ProjectRepository {
            -ProjectAdapter _adapter
            -DbContext _dbContext
            +GetById(id) Project
            +Save(project: Project) void
            +DeleteById(id) void
        }
    }

    namespace Service {
        class ProjectService {
            -IProjectRepository _repository
            +GetById(id) Project
            +Save(project: Project) void
            +Search(args) IList~Project~
        }
    }

    ProjectService --> IProjectRepository : calls with trees
    IProjectRepository <|.. ProjectRepository : implements
    ProjectRepository --> ProjectAdapter : chop / reassemble
    ProjectRepository --> ProjectEntity : EF Core operations
    ProjectAdapter --> Project : knows the tree
    ProjectAdapter --> ProjectEntity : knows the boxes
    ProjectService --> Project : works with trees
    Project *-- ProjectClass : branch
    ProjectEntity "1" --> "*" ProjectClassEntity : FK ProjectId

Look at the separation. ProjectService works with Project trees and calls IProjectRepository. That's it. It doesn't know about ProjectEntity boxes. It doesn't know about EF Core. It doesn't know about adapters. It calls repository methods that accept and return living trees.

ProjectRepository is where the chopping and reassembly happens — and it's the only place it happens. It takes a ProjectAdapter and a DbContext through dependency injection. When the service calls Save(project), the repository uses the adapter to chop the Project tree into ProjectEntity boxes, then stores the boxes via EF Core. When the service calls GetById(id), the repository pulls ProjectEntity boxes from the warehouse, uses the adapter to reassemble them into a Project tree, and returns the tree.

ProjectAdapter is the bridge between the two shapes. It has Adapt() methods that go in both directions. Tree-to-boxes for saves. Boxes-to-tree for reads. It's the only class that references both the tree namespace and the box namespace.

Notice what this buys you: if the boxes change — if ProjectEntity gets a new column, or a column gets renamed, or a table gets restructured — the only code that changes is the box class and the adapter. The service layer, the tree, the controllers, the tests — none of them change. The warehouse change is absorbed at the boundary where it belongs.

Why the Adapter Layer Is Critical to Unit Test

I want to make a point here that I feel strongly about, because I've seen the damage when teams skip it.

The adapter — the code that chops trees into boxes and reassembles boxes back into trees — is some of the most important code in your system to unit test. Not just "it would be nice." Critical path.

Here's why: the adapter is where the shape changes. And every shape change is a place where something can get lost.

Every property mapping is an assumption. "This branch on the tree maps to this column in the box." "This enum value maps to that string." "This nested collection on the tree maps to these rows in a child box with a foreign key." Each one of those assumptions is a place where bugs hide.

And these bugs are insidious. A mismatched property mapping doesn't throw an exception. It silently packs the wrong data into the wrong box, or silently puts a branch back in the wrong place on the reassembled tree — or completely fails to put the branch there at all. You find out weeks later when someone notices the order total is wrong, or the customer's address isn't updating, or the status field shows "Pending" when it should show "Shipped."

Remember: you're trying to reassemble a living tree from cardboard boxes. The tree that comes back out should match the tree that went in. But the conversion is fundamentally lossy — the tree structure got destroyed during packing, and you're relying on labels and conventions to put it back together. If a label is wrong, or a branch got packed in the wrong box, or a piece was quietly dropped — the reassembled tree looks right until someone leans on the branch that isn't really there.

The adapter layer is also where schema drift becomes visible. Over time, the boxes evolve. A column gets renamed. A new field gets added to a box that the tree doesn't need yet. A tree gains a calculated property that has no column. The adapter absorbs these differences — it's the buffer that lets both shapes change without breaking each other. But if someone adds a branch to the tree and forgets to update the adapter, you get a silent bug. A unit test that verifies "every branch on the tree round-trips correctly through the adapter" catches that the moment someone forgets.

The good news: adapter code is extremely unit testable. It's pure shape-conversion logic. No database dependency. No file system dependency. No network calls. Give it a tree, assert you get the right boxes. Give it boxes, assert you get the right tree. Test the edge cases — nulls, empty collections, enum values, default values. These tests are fast, deterministic, and they catch real bugs.

Unit tests don't have dependencies on running systems like SQL Server databases. Integration tests probably do have that dependency. Separating the adapter logic from the EF Core data storage logic means that you can test huge portions of really bug-prone code without having to spin up a database full of data.

Here's the kind of test I'm talking about:

[TestMethod]
public void AdaptProjectToEntity_MapsAllProperties()
{
    // Arrange
    var adapter = new ProjectAdapter();
    var tree = CreateSampleProject(); // all branches populated

    var box = new ProjectEntity();

    // Act — chop the tree, pack it into boxes
    adapter.Adapt(tree, box);

    // Assert — did every branch land in the right box?
    Assert.AreEqual(tree.Name, box.Name);
    Assert.AreEqual(tree.ProjectType, box.ProjectType);
    Assert.AreEqual(tree.Classes.Count, box.Classes.Count);
    // ... every property verified
}

[TestMethod]
public void AdaptEntityToProject_MapsAllProperties()
{
    // Arrange
    var adapter = new ProjectAdapter();
    var box = CreateSampleProjectEntity(); // all columns populated

    var tree = new Project();

    // Act — unpack the boxes, reassemble the tree
    adapter.Adapt(box, tree);

    // Assert — did every box column land on the right branch?
    Assert.AreEqual(box.Name, tree.Name);
    Assert.AreEqual(box.ProjectType, tree.ProjectType);
    Assert.AreEqual(box.Classes.Count, tree.Classes.Count);
    // ... every property verified
}

No database required. No EF Core. No startup configuration. Just "did the shape conversion work? Did the tree survive the round trip through the boxes?" Run in milliseconds.

I've walked into codebases where the adapter tests were the first tests I wrote — not because they were the most glamorous, but because they were where the most bugs were hiding. Every time I found a bug in production where "the data in the database doesn't match what the UI is showing," the root cause was in the adapter. Every time. Something got lost in the chopping, or something got put on the wrong branch during reassembly.

The Cost of All This

Let me add it up.

To build a well-designed .NET application with EF Core and SQL Server, you need:

Trees — domain model classes that represent your business. They have behavior, rules, and business-shaped hierarchy. They know nothing about boxes.

Boxes — EF entity classes that represent your database. They have foreign keys, navigation properties, and warehouse-shaped structure. They know nothing about trees.

Adapters — classes that chop trees into boxes and reassemble boxes into trees, handling nulls, collections, type conversions, and schema drift. They're the only code that knows about both shapes.

Repository interfaces that accept and return trees. They define what data operations are available without exposing that boxes exist behind the scenes.

Repository implementations that wire up EF Core, use the adapters, and implement the repository interfaces. This is the only code that touches DbContext and the warehouse.

Unit tests for the adapters to make sure every branch on the tree maps to the right column in the right box, in both directions, and that nothing gets lost in the conversion.

Integration tests for the repositories to make sure EF Core actually generates the SQL you expect against the real warehouse.

Service layer classes that orchestrate business logic using repository interfaces and trees. They don't know the warehouse exists.

ViewModels or DTOs at the presentation layer — because your trees aren't the right shape for your API responses or your Razor Pages either. (A different kind of shape conversion, but shape conversion all the same.)

That's a lot of infrastructure. And to be clear — it's the right infrastructure if you're building a serious application on a relational database. Every layer exists for a reason. Every shape conversion solves a real problem. This is good architecture. I've been building applications this way for years and I believe in it. Everything has a point that connects back to either maintainability or testability or both.

But it's worth pausing to notice how much of that infrastructure exists because of one fundamental fact: trees and boxes are different shapes. The impedance mismatch created by the relational model radiates outward into your entire architecture. Every adapter, every mapping, every translation step is a consequence of that core tension.

The boxes exist because the warehouse stores boxes. The trees exist because your code thinks in trees. The adapters exist because someone has to chop and reassemble. The repository interfaces exist to hide the boxes from everything else. The adapter tests exist because things get lost when you chop a living tree into flat boxes and try to reassemble it again. All of that — every line of it — is the cost of working with two different shapes.

And if you stop and think about it for a moment, there's something slightly absurd about the whole enterprise. You take a living tree — your domain model, with its hierarchy and its behavior and its meaning — and you turn it into pulp. You flatten it. You pack it into uniform, stackable boxes and file them in a warehouse. And then the next time someone needs that tree, you pull the boxes back out, open them up, and try to resurrect something living from something dead. You do this hundreds of times a second. You write tests to make sure the reconstruction works. You build entire architectural layers to manage the chopping and reassembly.

You do all of this because fifty-something years ago, disk space cost $700,000 a gigabyte and we couldn't afford to store a tree as a tree.

But what if it didn't have to be this way?

What if the database could store the tree as-is? Living structure intact. Branches in place. No chopping. No boxes. No reassembly. What if your domain objects — with their hierarchy, their nesting, their business-shaped branching — could be the stored documents?

No adapter layer. No entity classes. No warehouse. No impedance mismatch. Just the tree.

That's where Cosmos DB comes in and that's what the next chapter is about.