Introduction
Have you ever spent hours tracking down a bug, only to discover that another piece of code had changed an object while it was being passed around? A method you thought only read data also modified its state, affecting later steps.
Imagine a Web API that receives an HTTP request and converts its contents into a request DTO. The data then passes through a domain object that handles business rules and a persistence model used for database access, before the API produces a response DTO. If one step changes data that you expected to preserve, later steps may produce unexpected results.
Note: DTO stands for data transfer object. A request DTO carries data from an external request; a response DTO carries data returned by an API.
The following diagram shows one possible data flow from HTTP request to response. When every layer can change object state, the data’s origin and the responsibility for changing it become increasingly difficult to trace.

Note: In the diagram, “Domain Object” represents core business concepts and rules. “Persistence Entity” is a storage model tailored to a database or data-access tool. This is one common division of responsibilities, not a requirement that every system use separate types or a particular layered architecture.
In this flow:
- The Request DTO may be modified.
- The Domain Object may be modified.
- The Persistence Entity may be modified by an ORM (object-relational mapper) or other code.
- The Response DTO may be based on state that has already changed.
What value does a particular property hold now? Who changed it? In which layer?
These questions share a common source: mutable state flows through the program, making it hard to tell what values an object held at a particular moment, who changed those values, and where.
That is the problem this book explores and aims to address.
Separate models can clarify each step’s responsibilities. Yet even within a single flow, a later step may change data that an earlier step already validated, or overwrite state you intended to preserve. If other workflows also share the object, the effects become harder to see.
This book demonstrates another approach: keep data unchanged after creation when it should remain stable, and explicitly produce a new version when a change is needed. Callers can use the transformed result without changing the original data, and each step becomes easier to test.
Not a silver bullet
This book does not recommend abandoning object-oriented programming (OOP). OOP remains useful for encapsulating complex business rules, managing entities throughout their lifecycles, and enforcing rules that objects must always satisfy. These situations still need object-oriented encapsulation.
It is also not a book about functional programming. You do not need prior knowledge of functional programming to read it and apply its methods. We start with concrete data flows and consider where immutable design is worthwhile and what it costs.
Developing judgment
C#’s record, init, with, immutable collections, and pattern matching make it easier to build immutable data models and express state changes as transformations. You may know each tool but still be unsure how to combine them in real business workflows. Through examples, this book explains how to organize data creation, validation, and transformation, turning syntax knowledge into design techniques you can use in a project.
The book does not recommend turning every type into a record or treating “moving every method into a service class” as good design. Immutable models can have behavior. Where data and behavior belong still depends on business rules, how callers use the model, and maintenance costs.
My aim is to help you develop the judgment to decide whether immutable design can help with a particular piece of code or design problem and, if so, how to apply it. As the examples progress, we will assess which changes make the code clearer and where the existing approach is sufficient.
Let’s start with a basic question: what problems do mutable objects actually cause?