Decimal is the de facto standard Elixir library for arbitrary-precision decimal arithmetic. When you're handling money — processing payments, computing tax, splitting bills, or converting currencies — floating-point math with Float is disqualifying. A Float addition like 0.1 + 0.2 yields 0.30000000000000004; the same addition with Decimal.add("0.1", "0.2") yields 0.3 exactly. Financial services built on Decimal depend on the correctness of every computation, on the availability of exchange rate APIs they call, and on the health of the accounting records they write. Vigilmon adds external uptime monitoring, heartbeat checks, and alerting so the infrastructure surrounding your Decimal-based financial computations stays healthy and any degradation surfaces before it affects customer balances.
What You'll Set Up
- HTTP health endpoint confirming your pricing and calculation service is reachable
- Heartbeat monitor for scheduled financial reconciliation jobs
- Exchange rate API uptime monitor to detect when your currency data source goes dark
- SSL monitoring for your payment provider endpoints
- Slack alerts when any financial service layer degrades
Prerequisites
- Elixir 1.14+ with
decimalinmix.exs - A Phoenix or OTP application processing monetary values with Decimal
- A free Vigilmon account
Step 1: Verify Your Decimal Configuration
Decimal uses a configurable context that sets precision and rounding mode. Financial applications should pin these at startup to prevent precision drift:
# lib/my_app/application.ex
defmodule MyApp.Application do
use Application
def start(_type, _args) do
# Set a global Decimal context with 28-digit precision and banker's rounding
Decimal.Context.set(%Decimal.Context{
precision: 28,
rounding: :half_even, # Banker's rounding — standard for financial calculations
flags: [],
traps: [:invalid_operation, :division_by_zero]
})
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.ExchangeRateCache,
# ...
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
Confirm the context is applied in a test:
# test/my_app/decimal_context_test.exs
defmodule MyApp.DecimalContextTest do
use ExUnit.Case
test "global context has financial precision" do
ctx = Decimal.Context.get()
assert ctx.precision == 28
assert ctx.rounding == :half_even
end
test "monetary addition is exact" do
result = Decimal.add("0.1", "0.2")
assert Decimal.equal?(result, Decimal.new("0.3"))
end
test "tax calculation rounds correctly" do
# 7.5% tax on $10.00 = $0.75
price = Decimal.new("10.00")
rate = Decimal.new("0.075")
tax = Decimal.mult(price, rate)
assert Decimal.equal?(Decimal.round(tax, 2), Decimal.new("0.75"))
end
end
Step 2: Add a Health Endpoint for Your Pricing Service
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
checks = %{
decimal_context: check_decimal_context(),
exchange_rate_cache: check_exchange_rate_cache(),
database: check_database(),
calculation_smoke_test: check_calculation()
}
status = if Enum.all?(checks, fn {_, v} -> v == :ok end), do: 200, else: 503
conn
|> put_status(status)
|> json(%{
status: if(status == 200, do: "ok", else: "degraded"),
checks: checks
})
end
defp check_decimal_context do
ctx = Decimal.Context.get()
if ctx.precision >= 20 and ctx.rounding == :half_even do
:ok
else
:misconfigured
end
end
defp check_exchange_rate_cache do
case MyApp.ExchangeRateCache.get("USD", "EUR") do
{:ok, rate} when is_struct(rate, Decimal) -> :ok
_ -> :stale_or_unavailable
end
end
defp check_database do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> :ok
_ -> :error
end
end
defp check_calculation do
# Smoke-test a known calculation with an exact expected result
price = Decimal.new("99.99")
discount = Decimal.new("0.15")
discounted = Decimal.mult(price, Decimal.sub(Decimal.new("1"), discount))
expected = Decimal.new("84.9915")
if Decimal.equal?(discounted, expected), do: :ok, else: :calculation_mismatch
end
end
Register the route:
# lib/my_app_web/router.ex
scope "/", MyAppWeb do
get "/health", HealthController, :index
end
Test locally:
curl -s http://localhost:4000/health | jq .
# {
# "status": "ok",
# "checks": {
# "decimal_context": "ok",
# "exchange_rate_cache": "ok",
# "database": "ok",
# "calculation_smoke_test": "ok"
# }
# }
Step 3: Cache Exchange Rate Data with a GenServer
Decimal calculations are only as reliable as the exchange rate data they operate on. Cache rates and surface cache health to Vigilmon:
# lib/my_app/exchange_rate_cache.ex
defmodule MyApp.ExchangeRateCache do
use GenServer
require Logger
@refresh_interval_ms :timer.minutes(15)
def start_link(_opts) do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
end
def get(from_currency, to_currency) do
key = {from_currency, to_currency}
GenServer.call(__MODULE__, {:get, key})
end
@impl GenServer
def init(_) do
send(self(), :refresh)
{:ok, %{rates: %{}, last_updated: nil}}
end
@impl GenServer
def handle_call({:get, {from, to}}, _from, state) do
case Map.get(state.rates, {from, to}) do
nil -> {:reply, {:error, :not_found}, state}
rate -> {:reply, {:ok, rate}, state}
end
end
@impl GenServer
def handle_info(:refresh, state) do
new_state = case fetch_rates() do
{:ok, rates} ->
%{state | rates: rates, last_updated: DateTime.utc_now()}
{:error, reason} ->
Logger.error("Exchange rate refresh failed: #{inspect(reason)}")
state
end
Process.send_after(self(), :refresh, @refresh_interval_ms)
{:noreply, new_state}
end
defp fetch_rates do
# Replace with your exchange rate provider
case Req.get("https://api.exchangerate-api.com/v4/latest/USD", receive_timeout: 10_000) do
{:ok, %{status: 200, body: %{"rates" => rates}}} ->
decimal_rates =
Map.new(rates, fn {currency, rate} ->
{{"USD", currency}, Decimal.from_float(rate)}
end)
{:ok, decimal_rates}
{:ok, %{status: status}} ->
{:error, {:http_error, status}}
{:error, reason} ->
{:error, reason}
end
end
end
Step 4: Set Up HTTP Monitoring in Vigilmon
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- URL:
https://yourdomain.com/health - Expected status:
200 - Add a keyword check:
"decimal_context":"ok"to confirm the global Decimal context is properly configured - Interval: 1 minute
- Save
Add a second keyword check for "calculation_smoke_test":"ok" on the same monitor — this catches precision regressions where the Decimal library behaves unexpectedly after a dependency upgrade.
Monitor your exchange rate API source:
- Click New Monitor → HTTP
- URL:
https://api.exchangerate-api.com/v4/latest/USD(or your provider's URL) - Expected status:
200 - Add a keyword check:
"rates"to confirm the response contains rate data - Interval: 5 minutes
- Save
If the exchange rate API goes dark, your cache serves stale rates. Vigilmon alerts you before the staleness becomes a customer-impacting problem.
Step 5: Heartbeat Monitoring for Reconciliation Jobs
Financial applications typically run nightly reconciliation to verify that computed totals match ledger entries. Add a heartbeat so Vigilmon detects when reconciliation stops running:
# lib/my_app/workers/reconciliation_worker.ex
defmodule MyApp.Workers.ReconciliationWorker do
use Oban.Worker, queue: :reconciliation, max_attempts: 1
require Logger
@impl Oban.Worker
def perform(%Oban.Job{}) do
date = Date.utc_today() |> Date.add(-1)
case MyApp.Accounting.reconcile_day(date) do
{:ok, %{discrepancies: 0}} ->
ping_heartbeat()
:ok
{:ok, %{discrepancies: count}} ->
Logger.error("Reconciliation for #{date}: #{count} discrepancies found")
# Don't ping heartbeat — alert Vigilmon by not pinging
{:error, :discrepancies_found}
{:error, reason} ->
Logger.error("Reconciliation failed for #{date}: #{inspect(reason)}")
{:error, reason}
end
end
defp ping_heartbeat do
url = Application.get_env(:my_app, :vigilmon)[:reconciliation_heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, _} -> :ok
{:error, reason} ->
Logger.warning("Vigilmon heartbeat ping failed: #{inspect(reason)}")
end
end
end
end
In Vigilmon:
- Click New Monitor → Heartbeat
- Name:
nightly-reconciliation - Expected interval: 24 hours
- Copy the ping URL and set it as
VIGILMON_RECONCILIATION_HEARTBEAT_URL
Note: the heartbeat is intentionally not pinged when discrepancies are found — Vigilmon's missed heartbeat alert then acts as both a "reconciliation didn't run" alert and a "reconciliation found problems" alert.
Step 6: SSL Monitoring for Payment Provider Endpoints
Decimal powers your internal calculations, but the endpoints your application calls to submit those calculations — payment gateways, banking APIs, fraud detection services — all need SSL monitoring:
- New Monitor → SSL Certificate →
https://api.stripe.com - New Monitor → SSL Certificate →
https://api.paypal.com - New Monitor → SSL Certificate →
https://yourdomain.com(your own service) - Alert threshold: 14 days before expiry
Step 7: Alert on Financial Service Degradation via Slack
- In Vigilmon: Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- Enable it on all monitors:
/health, exchange rate API, reconciliation heartbeat, SSL monitors
Use separate Slack channels for different severity levels — exchange rate API failures and SSL expiry warrant a different channel (or on-call page) than normal service health:
#monitoring-alerts (health check failures, heartbeat misses)
#monitoring-critical (SSL expiry, reconciliation discrepancies)
What You've Built
| What | How |
|------|-----|
| Decimal context verification | Health check confirming 28-digit precision and banker's rounding |
| Calculation smoke test | Health endpoint that verifies a known Decimal computation |
| Exchange rate cache health | GenServer freshness check in /health |
| Exchange rate API monitoring | HTTP monitor with keyword check on provider API |
| Reconciliation monitoring | Heartbeat only pinged on clean reconciliation, not on discrepancies |
| Payment provider SSL | Certificate expiry alerts for all financial endpoints |
Decimal guarantees correctness at the arithmetic level. Vigilmon guarantees the services, data sources, and scheduled jobs that surround your financial calculations stay healthy.
Next Steps
- Add a Vigilmon response-time monitor on your
/healthendpoint — latency growth in financial APIs often precedes availability failures - Use Vigilmon's status page to communicate planned maintenance windows for financial services to customers
- Add a keyword check for
"exchange_rate_cache":"ok"on your/healthmonitor separately from the Decimal context check, so you can distinguish the two failure modes in alerts - Consider adding a synthetic transaction test as a heartbeat: run a full pricing calculation end-to-end and ping only on exact expected output
Get started free at vigilmon.online.