{"id":6413,"date":"2026-07-18T10:55:25","date_gmt":"2026-07-18T10:55:25","guid":{"rendered":"https:\/\/lockitsoft.com\/?p=6413"},"modified":"2026-07-18T10:55:25","modified_gmt":"2026-07-18T10:55:25","slug":"the-end-of-the-mutable-dto-why-c-records-are-the-future-of-data-contracts","status":"publish","type":"post","link":"https:\/\/lockitsoft.com\/?p=6413","title":{"rendered":"The End of the Mutable DTO: Why C# Records Are the Future of Data Contracts"},"content":{"rendered":"<p>For years, C# developers have relied on mutable classes with <code>get; set;<\/code> properties to represent Data Transfer Objects (DTOs). This ubiquitous pattern, ingrained since the .NET Framework 2.0 era, has served as the connective tissue for countless distributed systems. However, an uncomfortable truth has emerged: mutable classes are fundamentally the wrong data structure for DTOs, a fact underscored by the introduction of a superior alternative in C# 9. This isn&#8217;t a mere stylistic quibble; it&#8217;s a design flaw that manifests in tangible production incidents, from duplicate financial transactions to corrupted audit trails and insidious race conditions. This article will explore the foundational reasons why records are the idiomatic and safer choice for DTOs, drawing parallels with critical systems in banking and fintech, where the cost of errors is measured in significant financial losses, not just software bugs. Whether you&#8217;re a junior engineer crafting your first API contract or a seasoned architect designing a payment platform, understanding the &quot;why&quot; behind records is paramount.<\/p>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_82_2 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#What_Are_DTOs_in_Practice\" >What Are DTOs in Practice?<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#The_Real_Problems_with_Class-Based_DTOs\" >The Real Problems with Class-Based DTOs<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#1_Mutability_is_a_Liability_at_Boundaries\" >1. Mutability is a Liability at Boundaries<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#2_Reference_Equality_is_the_Wrong_Equality\" >2. Reference Equality is the Wrong Equality<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#3_The_Boilerplate_Hides_the_Contract\" >3. The Boilerplate Hides the Contract<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#4_Lack_of_Intent_Communication\" >4. Lack of Intent Communication<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Enter_Records_The_Type_That_Says_What_It_Means\" >Enter Records: The Type That Says What It Means<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Records_in_the_Wild_Enterprise_Fintech_Scenarios\" >Records in the Wild: Enterprise Fintech Scenarios<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Scenario_1_Idempotent_Webhook_Ingestion\" >Scenario 1: Idempotent Webhook Ingestion<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Scenario_2_Event-Driven_Architecture_on_Kafka\" >Scenario 2: Event-Driven Architecture on Kafka<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Scenario_3_Maker-Checker_Workflows\" >Scenario 3: Maker-Checker Workflows<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Scenario_4_End-of-Day_Reconciliation\" >Scenario 4: End-of-Day Reconciliation<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#How_Records_Fulfill_Software_Engineering_Principles\" >How Records Fulfill Software Engineering Principles<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Performance_Considerations\" >Performance Considerations<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Limitations_and_Sharp_Edges\" >Limitations and Sharp Edges<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#When_a_Class_Is_Still_the_Right_Answer\" >When a Class Is Still the Right Answer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#A_Practical_Decision_Guide\" >A Practical Decision Guide<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/lockitsoft.com\/?p=6413\/#Conclusion_This_Was_Never_About_Syntax\" >Conclusion: This Was Never About Syntax<\/a><\/li><\/ul><\/nav><\/div>\n<h2><span class=\"ez-toc-section\" id=\"What_Are_DTOs_in_Practice\"><\/span>What Are DTOs in Practice?<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>At its core, a DTO serves as a simple, structured data carrier, designed to efficiently transfer data across system boundaries. Consider the seemingly straightforward example of a ride-sharing application. When a user requests nearby drivers, the backend might return a <code>DriverDto<\/code> containing essential information:<\/p>\n<pre><code class=\"language-json\">\n  \"id\": 142,\n  \"name\": \"Linda\",\n  \"rating\": 4.9\n<\/code><\/pre>\n<p>In C#, this might traditionally be represented as:<\/p>\n<pre><code class=\"language-csharp\">public class DriverDto\n\n    public int Id  get; set; \n    public string Name  get; set; \n    public double Rating  get; set; \n<\/code><\/pre>\n<p>While functional, this class-based approach is inherently mutable and verbose. It doesn&#8217;t accurately reflect the true nature of a DTO: a static snapshot of data at a particular moment.<\/p>\n<p>The real value of DTOs, however, shines in the complex landscapes of enterprise software, particularly within financial systems. In a payment platform, DTOs are indispensable for representing every facet of a transaction:<\/p>\n<ul>\n<li><strong>Payment Initiation:<\/strong> Details of a transaction being started.<\/li>\n<li><strong>Transaction Confirmation:<\/strong> Acknowledgment of successful payment processing.<\/li>\n<li><strong>Settlement Notifications:<\/strong> Updates on the final settlement of funds.<\/li>\n<li><strong>Reconciliation Records:<\/strong> Data points for matching internal and external ledgers.<\/li>\n<li><strong>Fraud Alerts:<\/strong> Information flagging potentially suspicious activity.<\/li>\n<li><strong>Audit Logs:<\/strong> Records of significant events for compliance and traceability.<\/li>\n<\/ul>\n<p>Each of these represents a contract, a definitive statement of fact communicated across a boundary. The fundamental insight is this: <strong>A DTO is a snapshot of data at a point in time. Snapshots do not change.<\/strong> Therefore, why are we modeling them with a type whose defining characteristic is its mutability?<\/p>\n<p>Imagine a printed bank statement. Once it leaves the printer, its contents are fixed. If a correction is needed, a new statement is issued. Mutable classes, with their <code>set;<\/code> accessors, are akin to handing out pens with every statement, inviting unauthorized alterations. Records, conversely, take the pen away, preserving the integrity of the data.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"The_Real_Problems_with_Class-Based_DTOs\"><\/span>The Real Problems with Class-Based DTOs<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The issues with mutable class-based DTOs extend beyond mere verbosity. They introduce concrete liabilities at system boundaries, leading to predictable failures:<\/p>\n<h3><span class=\"ez-toc-section\" id=\"1_Mutability_is_a_Liability_at_Boundaries\"><\/span>1. Mutability is a Liability at Boundaries<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Consider a <code>PaymentNotificationDto<\/code> flowing through a fintech platform. This object will traverse multiple services \u2013 validation, enrichment, fraud screening, ledger posting, and notification. These stages often involve asynchronous operations or even cross-thread communication. The inherent mutability of a class means that any of these stages can <em>silently<\/em> alter the DTO&#8217;s state.<\/p>\n<p>A seemingly innocuous code change, such as normalizing an amount:<\/p>\n<pre><code class=\"language-csharp\">\/\/ \"Normalizing\" inside the fraud check \u2013 seems harmless\nnotification.Amount = Math.Round(notification.Amount, 2);<\/code><\/pre>\n<p>can have cascading consequences. The amount that eventually lands on the ledger is no longer the original amount received from the partner bank. In high-volume financial systems, a minuscule discrepancy of &acirc;&#8218;&brvbar;0.005 across millions of transactions can lead to an end-of-day reconciliation nightmare, consuming valuable engineering hours to pinpoint the source of the alteration. The compiler, in this scenario, offers no protection. The immutable nature of financial data in transit is paramount, and mutability is an open invitation to tampering, whether accidental or malicious.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"2_Reference_Equality_is_the_Wrong_Equality\"><\/span>2. Reference Equality is the Wrong Equality<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>For all practical purposes, two DTOs carrying identical data represent the same &quot;fact.&quot; However, standard C# classes compare by reference.<\/p>\n<pre><code class=\"language-csharp\">var a = new PaymentNotificationDto  TransactionRef = \"TXN-8842\", Amount = 50000m, Currency = \"NGN\" ;\nvar b = new PaymentNotificationDto  TransactionRef = \"TXN-8842\", Amount = 50000m, Currency = \"NGN\" ;\n\nConsole.WriteLine(a == b);        \/\/ false\nConsole.WriteLine(a.Equals(b));   \/\/ false<\/code><\/pre>\n<p>This discrepancy is critical for <strong>idempotency<\/strong>, a cornerstone of financial systems. Partner banks may retry webhooks, Kafka delivers messages at-least-once, and mobile applications resubmit requests on timeouts. The system must reliably recognize &quot;I&#8217;ve processed this exact fact before.&quot; With mutable classes, achieving this requires either manually implementing <code>Equals<\/code> and <code>GetHashCode<\/code> (a common source of bugs when properties are added or removed without updating the equality logic) or resorting to less efficient methods like comparing serialized JSON.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"3_The_Boilerplate_Hides_the_Contract\"><\/span>3. The Boilerplate Hides the Contract<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>A class-based DTO, when properly implemented with immutability (via constructor injection), robust equality, and a useful <code>ToString<\/code> for logging, can easily balloon to 40-60 lines. The actual contract \u2013 the essential data structure \u2013 might only be 5 lines. The remaining 90% is boilerplate that must be maintained, reviewed, and kept synchronized. In an enterprise codebase with hundreds of DTOs, this amounts to tens of thousands of lines of code that serve no functional purpose beyond maintaining a legacy pattern, each line a potential hiding place for bugs.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"4_Lack_of_Intent_Communication\"><\/span>4. Lack of Intent Communication<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>The declaration <code>public class PaymentNotificationDto<\/code> provides no inherent information about how the type is intended to be used. Is it safe to cache? Can it be shared across threads? Is mutation expected? This ambiguity leaves the responsibility of understanding usage patterns to tribal knowledge and code review vigilance \u2013 two of the least reliable enforcement mechanisms in software development.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Enter_Records_The_Type_That_Says_What_It_Means\"><\/span>Enter Records: The Type That Says What It Means<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>C# records offer a paradigm shift, elegantly addressing these shortcomings. The same <code>PaymentNotificationDto<\/code> transformed into a record:<\/p>\n<pre><code class=\"language-csharp\">public record PaymentNotificationDto(\n    string TransactionRef,\n    decimal Amount,\n    string Currency,\n    string SenderAccount,\n    DateTime ReceivedAt);<\/code><\/pre>\n<p>This concise five-line definition automatically generates crucial functionality:<\/p>\n<ul>\n<li><strong>Value-based equality:<\/strong> <code>a == b<\/code> now correctly evaluates to <code>true<\/code> if the data is identical.<\/li>\n<li><strong><code>GetHashCode<\/code> implementation:<\/strong> Essential for efficient use in hash-based collections.<\/li>\n<li><strong><code>ToString()<\/code> override:<\/strong> Provides a structured, readable representation for logging.<\/li>\n<li><strong>Immutability:<\/strong> Properties are effectively <code>init<\/code>-only after construction.<\/li>\n<\/ul>\n<p>The idempotency check becomes trivial:<\/p>\n<pre><code class=\"language-csharp\">var a = new PaymentNotificationDto(\"TXN-8842\", 50000m, \"NGN\", \"0123456789\", timestamp);\nvar b = new PaymentNotificationDto(\"TXN-8842\", 50000m, \"NGN\", \"0123456789\", timestamp);\n\nConsole.WriteLine(a == b); \/\/ true \u2013 same fact, recognized as such<\/code><\/pre>\n<p>Furthermore, the attempt to mutate an amount that previously caused a subtle bug now results in a <strong>compile-time error<\/strong>:<\/p>\n<pre><code class=\"language-csharp\">notification.Amount = Math.Round(notification.Amount, 2);\n\/\/ CS8852: Init-only property can only be assigned in an object initializer<\/code><\/pre>\n<p>The <code>with<\/code> expression provides a controlled mechanism for creating modified copies, explicitly signaling a transformation:<\/p>\n<pre><code class=\"language-csharp\">var normalized = notification with  Amount = Math.Round(notification.Amount, 2) ;<\/code><\/pre>\n<p>This transformation from implicit mutation to explicit creation of a new state is invaluable in regulated environments where auditing the precise lineage of data is critical.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Records_in_the_Wild_Enterprise_Fintech_Scenarios\"><\/span>Records in the Wild: Enterprise Fintech Scenarios<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Let&#8217;s examine how records address real-world challenges in enterprise fintech:<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Scenario_1_Idempotent_Webhook_Ingestion\"><\/span>Scenario 1: Idempotent Webhook Ingestion<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In a payment system, duplicate settlement notifications due to network unreliability can lead to direct financial losses. Records, with their inherent value equality, simplify deduplication:<\/p>\n<pre><code class=\"language-csharp\">public record SettlementNotification(\n    string ProviderRef,\n    string SessionId,\n    decimal Amount,\n    string Currency,\n    string BeneficiaryAccount,\n    DateTimeOffset SettledAt);\n\n\/\/ In-memory dedup for a processing window \u2013 HashSet just works\nprivate readonly HashSet&lt;SettlementNotification&gt; _seenInWindow = new();\n\npublic async Task HandleAsync(SettlementNotification notification)\n\n    if (!_seenInWindow.Add(notification))\n    \n        _logger.LogWarning(\"Duplicate settlement ignored: Notification\", notification);\n        return; \/\/ Structured log includes full payload via generated ToString\n    \n    await _ledger.PostAsync(notification);\n<\/code><\/pre>\n<p>Using a <code>HashSet&lt;SettlementNotification&gt;<\/code> immediately benefits from the record&#8217;s correct <code>GetHashCode<\/code> and <code>Equals<\/code> implementation, preventing duplicate entries. With classes, this would necessitate error-prone manual equality implementations.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Scenario_2_Event-Driven_Architecture_on_Kafka\"><\/span>Scenario 2: Event-Driven Architecture on Kafka<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In event-driven systems, events represent immutable facts about the past. Records align perfectly with this principle:<\/p>\n<pre><code class=\"language-csharp\">public record TransferInitiated(\n    Guid TransferId,\n    string SourceAccount,\n    string DestinationAccount,\n    decimal Amount,\n    string Currency,\n    DateTimeOffset InitiatedAt) : IDomainEvent;<\/code><\/pre>\n<p>When replaying events or building new projections, the guarantee that no handler could have mutated the event is crucial for deterministic outcomes. Records enforce this architectural rule at the compiler level.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Scenario_3_Maker-Checker_Workflows\"><\/span>Scenario 3: Maker-Checker Workflows<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Regulated operations often employ a maker-checker model. Records, combined with the <code>with<\/code> expression, elegantly model state transitions:<\/p>\n<pre><code class=\"language-csharp\">public record ReversalRequest(\n    Guid RequestId,\n    string TransactionRef,\n    decimal Amount,\n    string MakerId,\n    string? CheckerId,\n    ApprovalStatus Status,\n    DateTimeOffset CreatedAt,\n    DateTimeOffset? DecidedAt);\n\npublic ReversalRequest Approve(ReversalRequest request, string checkerId) =&gt;\n    request with\n    \n        CheckerId = checkerId,\n        Status = ApprovalStatus.Approved,\n        DecidedAt = _clock.UtcNow\n    ;<\/code><\/pre>\n<p>The original request remains untouched, and the approval process generates a new, distinct state. This immutability is vital for auditability, allowing for a clear before-and-after comparison of the request.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Scenario_4_End-of-Day_Reconciliation\"><\/span>Scenario 4: End-of-Day Reconciliation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Reconciliation is fundamentally a set comparison problem. Records, with their value equality, seamlessly integrate with LINQ for efficient comparisons:<\/p>\n<pre><code class=\"language-csharp\">public record ReconRow(string Ref, decimal Amount, string Currency, DateOnly ValueDate);\n\nvar internalRows = await _ledger.GetRowsAsync(date);\nvar providerRows = ParseProviderFile(file);\n\nvar missingOnOurSide = providerRows.Except(internalRows);  \/\/ they have it, we don't\nvar missingOnTheirSide = internalRows.Except(providerRows);  \/\/ we have it, they don't<\/code><\/pre>\n<p>LINQ operations like <code>Except<\/code>, <code>Intersect<\/code>, and <code>Distinct<\/code> function correctly out-of-the-box with records, eliminating the need for custom <code>IEqualityComparer<\/code> implementations.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"How_Records_Fulfill_Software_Engineering_Principles\"><\/span>How Records Fulfill Software Engineering Principles<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Records are more than just syntactic sugar; they embody fundamental software engineering principles:<\/p>\n<ul>\n<li><strong>Single Responsibility Principle (SRP):<\/strong> Records focus solely on data carriage, preventing them from becoming mutable workspaces that introduce side effects.<\/li>\n<li><strong>Open\/Closed Principle (OCP):<\/strong> The <code>with<\/code> expression allows for extension (creating new states from existing ones) without modification, fostering a more predictable and testable pipeline.<\/li>\n<li><strong>Liskov Substitution Principle (LSP):<\/strong> Records offer hierarchy-aware equality, correctly distinguishing between base and derived types, though composition is generally preferred for DTOs.<\/li>\n<li><strong>Interface Segregation Principle (ISP):<\/strong> Wide positional records naturally encourage decomposition into smaller, more focused contracts through composition, making them easier to manage and understand.<\/li>\n<li><strong>Dependency Inversion Principle (DIP):<\/strong> Immutable record contracts provide stable abstractions, enhancing the reliability of inter-service communication.<\/li>\n<li><strong>YAGNI (You Aren&#8217;t Gonna Need It):<\/strong> Records provide only the necessary capabilities for data carriers, avoiding speculative mutability.<\/li>\n<li><strong>DRY (Don&#8217;t Repeat Yourself):<\/strong> By moving boilerplate equality and <code>ToString<\/code> logic to the compiler, records eliminate redundant code.<\/li>\n<li><strong>Principle of Least Astonishment:<\/strong> Record equality behaves intuitively, reducing unexpected behavior and debugging time.<\/li>\n<li><strong>Fail Fast:<\/strong> Positional records, when combined with constructor validation, ensure data integrity at the point of creation, preventing invalid states from propagating.<\/li>\n<li><strong>DDD: Records are Value Objects:<\/strong> Records are the language-level embodiment of the Value Object pattern from Domain-Driven Design, intrinsically designed for comparison by value.<\/li>\n<li><strong>CQS\/CQRS Alignment:<\/strong> Immutable records perfectly align with the unidirectional flow of commands and queries in CQRS architectures, simplifying reasoning and testing.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"Performance_Considerations\"><\/span>Performance Considerations<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While records primarily offer correctness and maintainability benefits, they also bring performance advantages:<\/p>\n<ul>\n<li><strong>Efficient Equality:<\/strong> Compiler-generated equality checks are direct and allocation-free, vastly outperforming reflection-based or serialization-based comparisons.<\/li>\n<li><strong>Optimized Collections:<\/strong> Correct <code>GetHashCode<\/code> enables O(1) lookups in dictionaries and hash sets.<\/li>\n<li><strong>Lock-Free Sharing:<\/strong> Immutability allows multiple threads to access records concurrently without synchronization overhead, significantly boosting throughput in high-TPS systems.<\/li>\n<li><strong>Elimination of Defensive Copies:<\/strong> The safety of passing immutable records reduces the need for costly defensive copies.<\/li>\n<li><strong><code>record struct<\/code> for Hot Paths:<\/strong> For small, frequently used data structures, <code>readonly record struct<\/code> offers stack allocation and zero GC pressure.<\/li>\n<\/ul>\n<p>However, it&#8217;s crucial to acknowledge the costs: <code>with<\/code> expressions allocate new instances, and record equality is field-dependent. These are typically minor concerns for DTOs but should be considered in extremely performance-critical, tight loops.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Limitations_and_Sharp_Edges\"><\/span>Limitations and Sharp Edges<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>While records are the superior choice for DTOs, they are not universally applicable:<\/p>\n<ul>\n<li><strong>EF Core Entities:<\/strong> ORM entities have identity and lifecycle management that conflicts with record semantics. Use classes for EF Core entities and map to records at the boundary.<\/li>\n<li><strong>Collection Properties:<\/strong> Mutable collections within records break value equality. Use immutable collections or override equality explicitly.<\/li>\n<li><strong>Shallow Copying with <code>with<\/code>:<\/strong> Nested mutable objects are not deep-copied by <code>with<\/code>, potentially leading to shared mutable state. Ensure immutability throughout nested structures.<\/li>\n<li><strong>Serializer\/Framework Friction:<\/strong> Older tools may not fully support record binding.<\/li>\n<li><strong>Inheritance Equality:<\/strong> While safer than class-based equality, record inheritance can still surprise developers. Favor composition.<\/li>\n<li><strong>Wide Records:<\/strong> Records with many parameters become unwieldy. Decompose them or use nominal records with <code>init<\/code> properties.<\/li>\n<li><strong>Brownfield Migration:<\/strong> Migrating legacy mutable DTOs requires careful analysis to avoid introducing regressions.<\/li>\n<\/ul>\n<h2><span class=\"ez-toc-section\" id=\"When_a_Class_Is_Still_the_Right_Answer\"><\/span>When a Class Is Still the Right Answer<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>Classes remain appropriate for types that represent:<\/p>\n<ul>\n<li><strong>Actors:<\/strong> Entities with identity, lifecycle, and mutable behavior (e.g., services, domain aggregates, UI controllers).<\/li>\n<li><strong>Mutable Caches or Builders:<\/strong> Types intentionally designed for accumulation or modification within a specific scope.<\/li>\n<li><strong>EF Core Entities:<\/strong> As previously mentioned, their identity and change-tracking mechanisms necessitate mutable classes.<\/li>\n<\/ul>\n<p>The guiding principle is simple: <strong>Records are for facts. Classes are for actors.<\/strong><\/p>\n<h2><span class=\"ez-toc-section\" id=\"A_Practical_Decision_Guide\"><\/span>A Practical Decision Guide<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<table>\n<thead>\n<tr>\n<th style=\"text-align: left\">Type of Thing<\/th>\n<th style=\"text-align: left\">Use<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td style=\"text-align: left\">API request\/response contracts<\/td>\n<td style=\"text-align: left\"><code>record<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Kafka \/ message-bus events<\/td>\n<td style=\"text-align: left\"><code>record<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">MediatR commands, queries, results<\/td>\n<td style=\"text-align: left\"><code>record<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">DDD value objects<\/td>\n<td style=\"text-align: left\"><code>record<\/code> (with constructor validation)<\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Small hot-path carriers<\/td>\n<td style=\"text-align: left\"><code>readonly record struct<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">EF Core entities \/ aggregate roots<\/td>\n<td style=\"text-align: left\"><code>class<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Services, handlers, middleware<\/td>\n<td style=\"text-align: left\"><code>class<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Builders, accumulators, mutable caches<\/td>\n<td style=\"text-align: left\"><code>class<\/code><\/td>\n<\/tr>\n<tr>\n<td style=\"text-align: left\">Configuration (e.g., <code>IOptions<\/code>)<\/td>\n<td style=\"text-align: left\"><code>record<\/code> with <code>init<\/code> properties<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<h2><span class=\"ez-toc-section\" id=\"Conclusion_This_Was_Never_About_Syntax\"><\/span>Conclusion: This Was Never About Syntax<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>The <code>record<\/code> keyword offers a significant reduction in boilerplate, but its true value lies in its architectural implications. Mutable class DTOs necessitate discipline; records enforce correctness through compilation. In critical domains like finance, the cost of a mutated DTO transcends mere bugs, leading to tangible financial and regulatory repercussions. Every software engineering principle, from SRP to fail-fast and DDD value semantics, converges on the inherent suitability of immutable, value-comparable records for data crossing system boundaries. By embracing records, we take away the pen, let the compiler enforce integrity, and ultimately build more robust, maintainable, and secure systems.<\/p>\n<!-- RatingBintangAjaib -->","protected":false},"excerpt":{"rendered":"<p>For years, C# developers have relied on mutable classes with get; set; properties to represent Data Transfer Objects (DTOs). This ubiquitous pattern, ingrained since the .NET Framework 2.0 era, has served as the connective tissue for countless distributed systems. However, an uncomfortable truth has emerged: mutable classes are fundamentally the wrong data structure for DTOs, &hellip;<\/p>\n","protected":false},"author":18,"featured_media":6412,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[136],"tags":[138,2814,352,257,2813,139,1412,137],"class_list":["post-6413","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-software-development","tag-coding","tag-contracts","tag-data","tag-future","tag-mutable","tag-programming","tag-records","tag-software"],"_links":{"self":[{"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/posts\/6413","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/users\/18"}],"replies":[{"embeddable":true,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6413"}],"version-history":[{"count":0,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/posts\/6413\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=\/wp\/v2\/media\/6412"}],"wp:attachment":[{"href":"https:\/\/lockitsoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6413"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6413"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/lockitsoft.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6413"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}