Telerik blogs

Working with monetary values in C# seems simple until improper implementation introduces rounding errors, inconsistencies and financial losses that are difficult to detect. Learn best practices for coding financial systems in ASP.NET Core.

Imagine you need to implement a new financial module where money is the system’s core entity: payments, reimbursements, recurring fees, discounts, interest and taxes all involve it daily.

At first glance, the implementation seems simple. After all, you just need to create a few decimal properties, perform mathematical operations and persist the values to the database. Right? Not always.

Small implementation decisions can lead to major problems. In systems processing thousands or millions of transactions, differences of just a few cents can turn into significant financial losses. For this reason, treating money merely as a number is often a mistake. Monetary values should have their own behaviors, rules and constraints reflected in the code.

In this article, we will explore patterns and best practices for handling money in C# applications, ranging from choosing the right data type to creating a Value Object that makes the domain safer, more expressive and less prone to errors.

💸 Common Mistakes When Dealing with Monetary Values

Before discussing patterns, it is important to understand the most common errors made when implementing financial features. Seemingly harmless decisions during development are among the leading causes of monetary inconsistencies. Let’s look at some of the main ones.

Using double or float for Monetary Values

A frequent mistake is using floating-point types (double or float) to represent monetary values.

double value = 0.1 + 0.2;

Console.WriteLine(value);

Running in Fiddle: https://dotnetfiddle.net/TpFz0k

Double sum result

Note that the result was an extremely long value. This happens because double and float store values using binary floating-point representation, which cannot accurately represent many decimal numbers.

For financial calculations, always prefer the decimal type, which was designed to perform monetary operations with precision, as shown in the example below:

decimal value = 0.1m + 0.2m;

Console.WriteLine(value);

Running in Fiddle: https://dotnetfiddle.net/bmQSR6

Decimal sum result

Representing Money as a Primitive Type

Another mistake is treating money simply as a property of the decimal type:

public decimal Price { get; set; }

The problem with this approach is that it raises some important questions: Which currency does this value represent? Can negative values be used? How should rounding be handled? Can values in different currencies be added together?

A primitive type cannot enforce these business rules, making the system more vulnerable to errors. A more robust approach would be to use a Value Object, as we will see later on.

Rounding Rules Scattered Throughout the Application

Financial systems often have specific rounding requirements. In this context, it is not uncommon to find applications that perform rounding operations in various parts of the code, as shown in the example below.

var total = Math.Round(subtotal, 2);
var discount = Math.Round(discountValue, 2);
var tax = Math.Round(taxValue, 2);

When rounding rules change, every instance must be reviewed, increasing the risk of something being overlooked and potentially leading to financial losses.

A better approach is to centralize monetary operations in a dedicated class responsible for applying business rules consistently and explicitly, all in one place.

Allowing Invalid Monetary States

When implementing financial systems, it is common to have a domain that imposes constraints not always guaranteed by the code. For example, a product price cannot be negative, an invoice total cannot be less than zero, and a discount cannot exceed the order value.

When money is represented as a primitive type, it becomes easy to create invalid states. For example:

decimal productPrice = -100m;

The compiler accepts this, but the business rule does not. Therefore, a dedicated class for representing money can validate these rules when instances are created.

Mixing Business Logic with Monetary Calculations

Financial calculations often end up scattered across services, controllers and repositories. However, as the application evolves, this logic becomes duplicated and difficult to understand:

var subtotal = quantity * unitPrice; 
var discount = subtotal * 0.10m; 
var tax = subtotal * 0.15m; 
var total = subtotal - discount + tax;

A better approach would be to encapsulate the behavior of monetary values within dedicated domain objects. This would help maintain calculation consistency and facilitate the system’s evolution.

💹 Handling Monetary Values the Right Way

In the previous section, we looked at some of the main mistakes developers make when implementing systems that handle monetary values. Now, let’s explore the best practices and recommended approaches for building financial systems that are more reliable and less prone to errors.

💰 A Pattern Called Money

Martin Fowler, in his book Patterns of Enterprise Application Architecture, presents Money as a pattern for modeling monetary values. The idea is simple: money is not just a number. A monetary value is composed of, at least, two inseparable elements: The value (amount) and the currency.

This modeling prevents invalid situations, such as adding dollars to euros, for example, or comparing values of different currencies without adequate conversion.

Furthermore, an instance of Money must be immutable. Instead of changing its internal state, operations such as addition or subtraction return a new instance containing the result of the operation. This makes the code more predictable and reduces side effects.

Implementing Money in an ASP.NET Core Application

Imagine that we are developing a financial system to control a digital wallet. The system must record deposits and withdrawals and must consider different currencies. In addition to having an updated wallet balance, we need to keep a history of all transactions.

Following Martin’s pattern, it makes sense to model money as a domain concept, in this case, a Value Object, and not just as a primitive type.

To maintain the focus of the post, some implementation details will be omitted, but you can access the complete code in this GitHub repository: Wallet API source code.

Within the “Domain” layer, we could create the following entities: Money, Currency, WalletAccount and Transaction:

Money:

namespace Wallet.Api.Domain;

public sealed record Money
{
    public decimal Amount { get; }
    public string Currency { get; }

    public Money(decimal amount, string currency)
    {
        if (amount < 0)
            throw new ArgumentOutOfRangeException(nameof(amount));

        if (string.IsNullOrWhiteSpace(currency) || !Domain.Currency.IsValid(currency))
            throw new ArgumentException(nameof(currency));

        Amount = decimal.Round(amount, 2, MidpointRounding.ToEven);
        Currency = currency;
    }

    public Money Add(Money value)
    {
        EnsureSameCurrency(value);

        return new Money(Amount + value.Amount, Currency);
    }

    public Money Subtract(Money value)
    {
        EnsureSameCurrency(value);

        if (Amount < value.Amount)
            throw new InvalidOperationException("Insufficient balance.");

        return new Money(Amount - value.Amount, Currency);
    }

    private void EnsureSameCurrency(Money value)
    {
        if (Currency != value.Currency)
            throw new InvalidOperationException("Currencies must match.");
    }

    public override string ToString() => $"{Amount:N2} {Currency}";
}

Note that here Money is treated as a domain concept rather than a primitive type. This is precisely what Fowler proposes in his pattern. Money has its own business rules, such as currency, rounding and valid operations. So it should not be represented simply by a decimal number scattered throughout the application.

By encapsulating these rules within the Money class, we eliminate Primitive Obsession, one of the code smells described by Fowler. Instead of multiple methods accepting separate decimal and string arguments, there is a single object that accurately represents the concept of money.

Immutability is another key point. Both Amount and Currency have only getters and are defined exclusively in the constructor. The Add and Subtract methods never modify the current instance. Instead, they always return a new Money object.

Furthermore, according to Eric Evans, Value Objects should be immutable whenever possible. Fowler also recommends this approach for Money, as monetary values represent a mathematical concept. If two operations shared the same mutable instance, various hard-to-detect bugs could arise, such as issues with concurrent operations, unexpected side effects or the accidental alteration of shared balances. By always returning a new instance, the class makes such situations impossible.

Note also the Currency property, which illustrates one of the most important points of the Money pattern advocated by Fowler. Money is never just a number: the values 100 USD, 100 EUR and 100 BRL represent completely different things. This is precisely why Fowler states that Money consists of two inseparable pieces of information: Amount and Currency.

Another important point is that we use MidpointRounding.ToEven (known as Banker’s Rounding), which is frequently recommended in financial applications because it reduces the statistical bias caused by always rounding up.

Currency:

namespace Wallet.Api.Domain;
public static class Currency
{
    public const string USD = "USD";
    public const string EUR = "EUR";
    public const string BRL = "BRL";

    private static readonly HashSet<string> _valid = [USD, EUR, BRL];

    public static bool IsValid(string currency) => _valid.Contains(currency);
}

The Currency class plays a significant role within the domain. It centralizes the concept of valid application currencies, preventing arbitrary values from being used when creating a Money object.

A key point is that it avoids the use of “magic strings” scattered throughout the system. Without this class, one might commonly see code like new Money(50, "USD");. The problem with this approach is that typos go unnoticed by the compiler and can lead to serious application errors, such as new Money(100, "Us"); for example.

By defining a dedicated Currency class, the code becomes much safer and more expressive. In other words, it not only reduces typos but also communicates intent more clearly.

WalletAccount:

namespace Wallet.Api.Domain;

public class WalletAccount
{
    private WalletAccount()
    {
    }

    public Guid Id { get; }

    public Money Balance { get; private set; }

    public WalletAccount(Guid id, string currency)
    {
        Id = id;
        Balance = new Money(0, currency);
    }

    public void Deposit(Money amount)
    {
        Balance = Balance.Add(amount);
    }

    public void Withdraw(Money amount)
    {
        Balance = Balance.Subtract(amount);
    }
}

The WalletAccount class represents the core domain entity responsible for the digital wallet. Unlike Money, which is a Value Object, WalletAccount possesses its own identity, represented by an Id. In Domain-Driven Design, this is precisely the defining characteristic of an entity: it is recognized not merely by its attributes, but by a unique identity that remains constant throughout its lifecycle.

Note that we utilize the previously created Money object. Another key point is that the entity never allows the balance to be modified directly. The Balance property features a private setter, preventing any part of the application from arbitrarily assigning a new value. This preserves encapsulation and routes all changes through business methods defined by the entity itself.

The Deposit and Withdraw methods exemplify this behavior. Instead of exposing the balance for other classes to perform manual calculations, we define operations that make sense within the domain. This approach follows a key object-oriented principle known as Tell, Don’t Ask. Rather than retrieving the balance, performing calculations externally, and then assigning a new value, we simply instruct the wallet itself to execute the desired operation.

Another interesting detail is that the entity fully delegates money-related rules to the Money object. When a deposit is made, the wallet does not need to know how to add monetary values; it simply asks the Money object to perform that operation. The same applies to withdrawals. This results in a solid distribution of responsibilities: the wallet handles account-specific rules, while the Money object handles rules related to money itself.

WalletAccount manages the wallet’s behavior, whereas Money encapsulates everything regarding monetary values. Consequently, the model remains organized, easy to understand and simpler to evolve.

Transaction:

namespace Wallet.Api.Domain;

public enum TransactionType { Deposit, Withdraw }

public class Transaction
{
    private Transaction() { }

    public Guid Id { get; } = Guid.NewGuid();
    public Guid WalletAccountId { get; private set; }
    public TransactionType Type { get; private set; }
    public decimal Amount { get; private set; }
    public string Currency { get; private set; } = default!;
    public DateTime CreatedAt { get; private set; } = DateTime.UtcNow;

    public static Transaction Create(Guid walletAccountId, TransactionType type, Money money)
        => new() { WalletAccountId = walletAccountId, Type = type, Amount = money.Amount, Currency = money.Currency };
}

The Transaction class is responsible for recording an event that occurred within the domain: a deposit or a withdrawal. The entity is created via the static Create method. Rather than exposing a public constructor, we centralize all creation logic in a single place.

An interesting detail is that the method receives a Money object, yet the entity does not store it directly. Instead, it copies only the Amount and Currency to its own properties. This makes sense when considering financial history. A transaction represents an immutable record of an event that occurred at a specific moment. It does not need to maintain a reference to the Money object used during the operation, it only needs to persist the information representing that historical fact.

It is also important to note that the example shown in this post is for educational purposes, serving as a starting point. In production scenarios, transactions would require a more complex implementation, potentially utilizing asynchronous events via the Event Sourcing pattern.

⏳Saving Time with NodaMoney

NodaMoney is a .NET library inspired by the Money pattern described by Martin Fowler. It provides a ready-to-use implementation for representing monetary values, encapsulating concepts such as amount, currency, rounding, mathematical operations, currency comparison and formatting.

Instead of creating our own Money class, we can use the Money class provided by NodaMoney.

Note that if we run the code below in Fiddle, the result will be the value already correctly formatted in dollars, simply by using the NodaMoney NuGet package:

using System;
using NodaMoney;

var salary = new Money(5000m, "USD");

Console.WriteLine(salary);

Money with NodaMoney

Running in Fiddle: https://dotnetfiddle.net/XRYYMw

However, if we try to add two values in different currencies, we will get an error, verifying that no inconsistent additions occur:

using System;
using NodaMoney;

var salary = new Money(5000m, "EUR");
var bonus = new Money(1200m, "USD");

var total = salary + bonus;

Console.WriteLine(total);

Different currencies error

Running in Fiddle: https://dotnetfiddle.net/82TgT4

NodaMoney features automatic rounding:

using System;
using NodaMoney;

var value = new Money(10.999m, "USD");
Console.WriteLine(value);

NodaMoney round-ups

Running in Fiddle: https://dotnetfiddle.net/B1xoOs

NodaMoney features an interesting function for allocating amounts, used to split money without losing any cents.

Consider an example where we need to split $100 among three people.

using System;
using NodaMoney;

var prize = new Money(100m, "USD");

var shares = prize.Split(3);

foreach (var share in shares)
{
    Console.WriteLine(share);
}

This is important because it avoids rounding issues and ensures that the sum of the parts exactly equals the original amount.

NodaMoney split function

Running in Fiddle: https://dotnetfiddle.net/LN8eZV

🌱 Final Thoughts

In the examples presented in the section on common errors, money was treated merely as a decimal value. Any part of the code could alter these values, and the rules were scattered throughout the codebase.

After implementing Fowler’s Money pattern, we arrived at a model that clearly reflects the domain’s intent. Money became a Value Object, rules were centralized, invalid states were eliminated and the code began to better communicate its intent through methods and behaviors.

We also explored using NodaMoney to save development time, as it encapsulates the key functions for handling money safely in ASP.NET Core.

However, more important than avoiding rounding errors is building a model that makes errors difficult to occur in the first place. When the domain is responsible for financial rules, the application becomes more predictable and easier to evolve.

I hope this post has helped you understand how to implement resilient systems when dealing with monetary values.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.