ex_money: an opinionated money type for Elixir applications
Elixir implementation of Money with Currency
At a glance
- What is it?
- Money is an Elixir library for storing, converting and doing arithmetic on amounts that always carry a currency code, backed by Decimal arithmetic and the CLDR data in the localize package. It refuses mixed currency arithmetic, refuses automatic rounding, and version 6.0 replaced the ex_cldr dependency family with a single localize package, which is the migration cost to budget for.
- Who is it for?
- Money fits an Elixir team building accounting, invoicing or financial features where mixing currencies or silently rounding a fraction of a cent would be a defect, since the library makes both impossible by construction rather than by convention. It does not fit a project that needs exotic currency behaviour or that still depends on the ex_cldr backend modules, because version 6.0 removed the compile time backend system entirely.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 80 days ago.
- What is it written in?
- Mainly Elixir, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What the Money type guarantees and why it refuses to be flexible
Money implements a set of functions to store, retrieve, convert and perform arithmetic on a Money.t/0 type. That type is composed of an ISO 4217 currency code, an ISO 24165 digital token identifier for crypto currency, or a custom currency code, together with a currency amount.
The library describes itself as opinionated in the interests of serving as a dependable library that can underpin accounting and financial applications, and the opinions are enumerated. A Money must always have both an amount and a currency code or digital token identifier. The currency code must always be a valid ISO 4217 or ISO 24165 code, and both current and historical codes are accepted. Arithmetic can only be performed when both operands are of the same currency. Amounts are represented as a Decimal. And no rounding occurs automatically, unless a function expressly calls it out, as Money.split/2 does.
Those five rules are the design. A currency-aware integer with an implicit rounding step is what most money libraries get wrong, and Money removes the option instead of documenting it. If you have ever had a bug where a total drifted by a fraction because two code paths rounded differently, that is the class of problem this library is designed to make impossible.
The practical cost is that you cannot add two amounts in different currencies without converting first, and any helper you write that takes a bare number and a currency has to decide which currency it belongs to before it calls the library.
Currency data comes from CLDR through the localize package
The currency catalogue is not hand maintained. Money.known_currencies/0 returns all the ISO 4217 codes the library knows, known_current_currencies/0 returns the ones currently in use, known_historic_currencies/0 returns the historic ones, and known_tender_currencies/0 returns those known to be legal tender. DigitalToken.tokens/0 returns a map of the known ISO 24165 digital tokens.
Two separate jobs depend on that data. Explicit rounding obeys the rounding rules for a given currency, and those rules come from the Unicode consortium CLDR repository as implemented by the hex package localize. They define the number of fractional digits for a currency and the rounding increment where one applies. Output string formatting uses the same package so it rounds to the appropriate number of fractional digits and to the correct increment for currencies that have a minimum cash increment, with the Swiss Franc and Australian Dollar named as the examples.
That last point is the one that surprises people. A currency is not only a code and a number of digits. Some currencies have a smallest physical denomination, and rounding rules have to respect it. Getting this right by hand means maintaining a table of currency rules yourself, which is exactly the kind of table that goes stale.
Locale data is loaded lazily on first access, so a fresh install does not pay for every locale up front. If you previously used :preload_locales to warm the data at startup, the replacement is to run mix localize.download_locales at build time to pre-populate the locale cache for the configured :supported_locales.
Migrating from 5.x to 6.0 and dropping the ex_cldr backends
Version 6.0 is a breaking change and the migration is the biggest practical decision facing an adopter. Money 6.0 replaces the ex_cldr family of dependencies, which were ex_cldr, ex_cldr_numbers and ex_cldr_units, with the unified localize package. That change removes the compile-time backend system entirely, so the work is not just a dependency swap.
The first step is the dependency list in mix.exs:
# Remove these:
{:ex_cldr, "~> 2.46"},
{:ex_cldr_numbers, "~> 2.38"},
{:ex_cldr_units, "~> 3.19", optional: true},
# Add this:
{:localize, "~> 1.0"},Then you delete your CLDR backend module, because Localize has no compile-time backend system. A typical backend module looked like this and is meant to be removed:
# DELETE THIS MODULE
defmodule MyApp.Cldr do
use Cldr,
locales: ["en", "fr", "de"],
default_locale: "en",
providers: [Cldr.Number, Money]
endThe configuration follows, and note that the backend keys go away while the locale list moves into the localize configuration:
# Remove these from config.exs:
config :ex_money, default_cldr_backend: MyApp.Cldr
config :ex_cldr, default_backend: MyApp.Cldr
# Optionally configure localize locales:
config :localize,
default_locale: :en,
supported_locales: [:en, :fr, :de]After that, the :backend option has been removed from all Money functions, so calls to Money.new/3, Money.to_string/2, Money.parse/2 and Money.localize/2 lose that argument:
# Before (5.x):
Money.to_string(money, backend: MyApp.Cldr, locale: "de")
Money.parse("100 USD", backend: MyApp.Cldr)
# After (6.0):
Money.to_string(money, locale: "de")
Money.parse("100 USD")Locale management moves as well, from Cldr.put_locale and Cldr.get_locale to Localize.put_locale and Localize.get_locale, and custom currency creation moves from Cldr.Currency.new to Money.Currency.new.
Error tuples changed shape during the 6.0 migration
One migration detail is easy to miss because it does not live in the dependency list or the configuration file. The error types for currency validation changed, so any code pattern matching on the old error tuple silently stops matching.
# Before (5.x):
{:error, {Cldr.UnknownCurrencyError, "The currency :ZZZ is unknown"}}
# After (6.0):
{:error, {Money.UnknownCurrencyError, "The currency :ZZZ is not known."}}The module moves from Cldr.UnknownCurrencyError to Money.UnknownCurrencyError, and the message text changes as well. A rescue clause that used to catch the CLDR error will now let the error escape, which in a request path usually means a 500 rather than a handled validation failure. Grep for UnknownCurrencyError before you upgrade, and treat the message change as a second reason to avoid matching on the string.
The same migration applies to direct number formatting. If you call the number formatter yourself rather than going through Money, the module reference changes:
# Before (5.x):
Cldr.Number.to_string(1234, MyApp.Cldr, currency: :USD)
# After (6.0):
Localize.Number.to_string(1234, currency: :USD)This one has a double effect. The module changes, and the backend argument disappears, so a call that had three positional arguments becomes a call with two. Compiler warnings will catch the arity change, but they will not catch a call that still passes a backend, because that argument position no longer exists in the same shape.
The supervisor starts by default unless you opt out
Money starts a supervisor, Money.Supervisor, by default, unless the dependency is configured as runtime: false in mix.exs. If you take that option, the application can be started later with Money.Application.start(:normal, supervisor_options), where supervisor_options is a keyword list given to Supervisor.start_link/2. The default options are [strategy: :one_for_one, name: Money.Supervisor].
The reason this matters is the exchange rate retriever. The supervisor exists in the first place to hold the service process that fetches and caches exchange rates, and the documentation points to a dedicated section on the exchange rates service process supervision and startup for how to configure that retriever.
So there are two operating modes. The default is that adding the dependency also adds a running process tree, which is convenient but means your application starts a supervisor it did not explicitly ask for. The other mode is a library that starts nothing until you say so, which is what you want if the exchange rate source needs configuration from your own config, or if you want to control the startup order relative to your own supervisors.
One thing the documentation does not settle in the visible portion is the failure behaviour when the exchange rate backend is unreachable, since the retrieval details live in the section that is referenced rather than shown. If exchange rate availability matters to your release, read that section in the repository before you decide which mode to use.
Storing amounts in Postgres or MySQL without losing precision
Serialization is entirely optional, and the two supported database shapes reflect different type systems.
For Postgres, money can be serialised as a composite Postgres type that includes both the amount and the currency. That means the currency lives in the database row rather than in a separate column, so a query cannot read an amount without also reading which currency it is in. Given that the library's central rule is that an amount without a currency is invalid, storing them together is the shape that matches the type.
For MySQL, money is serialized into a json column with the amount converted to a string to preserve precision, since json does not have a decimal type. That conversion is the whole point and also the whole cost. A json column means you cannot ask the database to sum or compare amounts numerically without casting the string back, and a MySQL deployment will need to think about whether the application or the database owns those comparisons.
Choosing the storage model is therefore not a detail. Postgres with a composite type matches the library's guarantees. MySQL with a json column keeps the data intact but pushes arithmetic and comparison back into Elixir, and a query like a total across many rows stops being something the database can do well.
Where ex_money is the wrong choice
The clearest wrong case is a project that needs to mix currencies in one expression. Money arithmetic only happens between two operands of the same currency, so any application that sums a basket priced in dollars, euros and pounds has to convert first, and the rate source, the timing and the rounding of that conversion all become your problem.
The second wrong case is a codebase already built around the ex_cldr backend pattern with several locales and custom CLDR providers. The 6.0 release removes the compile-time backend system, so the backend module, the backend configuration keys and the :backend option all go. A team with one backend and two locales will find the migration an afternoon. A team that generated CLDR data per application and wired providers through it will find it a project.
The third is a project on an older Elixir. The library is supported on Elixir 1.17 and later only, so the runtime upgrade is a prerequisite rather than an option.
The fourth is a codebase that wants implicit rounding. Money performs no automatic rounding, which is correct for accounting, and inconvenient everywhere else. Any place that expects a division to hand back a rounded currency amount has to say so explicitly, and Money.split/2 is called out as the function that does round on purpose.
Compare that with handling money as a bare integer of minor units. That approach is simpler and faster, and it is what payment gateway integrations often hand you. The difference in approach is real: minor units avoid a decimal type and make rounding your problem by construction, while Money makes rounding explicit and applies CLDR rules so that the currency determines the answer rather than the developer.
Licence, release cadence and what the repository contains
The project is published on Hex as ex_money, and the repository metadata classifies the licence as a custom one that GitHub cannot categorise, with the terms in the LICENSE.md file at the root. That classification is not a warning sign in itself, but it does mean the licence text is not one of the familiar SPDX identifiers and should be read before you depend on it commercially. Read the file yourself rather than relying on a summary, and take your own advice on what it obliges you to do.
The release history shows steady work in the 6.x line. v6.1.1 came on 2026-07-04, v6.2.0 on 2026-07-31 and v6.2.1 on 2026-08-04. The repository is not archived, and the last push was on 2026-08-04, the same day as the newest tag.
The tree shows a project with real engineering discipline around it. There is a .credo.exs file for static analysis, a .dialyzer_ignore_warnings file, a .formatter.exs for the formatter, a .githooks directory, a config/ directory, a bench/ directory for benchmarks, a test/ directory, a plans/ directory, coveralls.json for coverage reporting, and a CHANGELOG.md.
That instrumentation tells you something about how the library expects to be maintained. The dialyzer ignore file in particular implies the team runs the type checker and records known exceptions rather than disabling it, which is a reasonable expectation for a library whose value rests on the accuracy of its currency data.
Editorial conclusion
Money fits an Elixir team building accounting, invoicing or financial features where mixing currencies or silently rounding a fraction of a cent would be a defect, since the library makes both impossible by construction rather than by convention. It does not fit a project that needs exotic currency behaviour or that still depends on the ex_cldr backend modules, because version 6.0 removed the compile time backend system entirely. Before adopting it, check that your Elixir version is 1.17 or later, plan the localize migration from ex_cldr, and decide whether Postgres composite type storage or the MySQL json column with a string amount suits the database you already run.
Frequently asked questions
What Elixir version does ex_money require?
Money is supported on Elixir 1.17 and later only. The library is published on Hex as ex_money, with the current release line in the 6.x series.
How do I migrate from ex_money 5.x to 6.0?
Replace the ex_cldr, ex_cldr_numbers and ex_cldr_units dependencies with the localize package, delete your CLDR backend module, remove the default_cldr_backend and default_backend configuration keys, and drop the :backend option from Money calls. Locale management moves from Cldr to Localize, and currency creation moves from Cldr.Currency.new to Money.Currency.new.
Does ex_money round currency amounts automatically?
No. All arithmetic functions work on a Decimal and no rounding occurs automatically, except where a function expressly calls it out, as Money.split/2 does. Explicit rounding follows the CLDR rules for the currency as implemented by the localize package.
How is money stored in the database with ex_money?
Serialization is optional. On Postgres, money can be stored as a composite type holding both the amount and the currency. On MySQL it goes into a json column with the amount converted to a string to preserve precision, since json has no decimal type.
Can I add two amounts in different currencies with ex_money?
No. Arithmetic can only be performed when both operands are of the same currency, so an amount always needs both a currency code or digital token identifier and an amount before it can be used.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ex-money-money)Was this useful?