Sitemap
Sulman Baig

Sulman Baig

Senior Software Engineer with 11 years in Ruby on Rails & Vue.js, specializing in health, e-commerce, staffing, and transport; skilled in software development.

Build a Currency Exchange Service in Ruby on Rails

3 min readDec 19, 2024

--

Press enter or click to view image in full size
Build a Currency Exchange Service in Ruby on Rails

Working with exchange rates in Rails applications can be tricky. After implementing currency handling in several production apps, I’ve developed a robust pattern that I’ll share with you today. We’ll build a service that fetches live exchange rates from Currency Layer API and integrates smoothly with Rails.

Get Sulman Baig’s stories in your inbox

Join Medium for free to get updates from this writer.

🎯 What We’ll Cover:

  • Setting up a reusable currency exchange service
  • Implementing background rate updates
  • Handling API integration gracefully
  • Testing our implementation

Project Setup

First, let’s add the necessary gems to our Gemfile:

gem 'httparty'  # For API requests
gem 'solid_queue' # For background jobs

Building the Currency Exchange Service

I prefer using Plain Old Ruby Objects (POROs) for API integrations. Here’s why — they’re easy to test, maintain, and modify. Let’s build our service:

# app/services/currency_exchange.rb
class CurrencyExchange
include HTTParty
base_uri 'https://api.currencylayer.com'

def initialize
@options = {
query: {
access_key: Rails.application.credentials.currency_layer.api_key
}
}
end

def self.list
new.list
end

def self.live
new.live
end
end

Pro Tip 💡 I’m using class methods ( self.list and self.live) as convenience wrappers around instance methods. This gives us flexibility - we can instantiate the class directly when we need to customize behavior, or use the class methods for quick access.

Handling API Responses

Let’s build clean value objects for our data:

class CurrencyExchange
# ...

GlobalCurrency = Struct.new(:code, :name)
Conversion = Struct.new(:from, :to, :rate)

def list
res = self.class.get('/list', @options)
return res.parsed_response['currencies'].map { |code, name|
GlobalCurrency.new(code, name)
} if res.success?
[]
end

def live
res = self.class.get('/live', @options)
return [] unless res.success?

res.parsed_response['quotes'].map do |code, rate|
Conversion.new(code[0..2], code[3..], rate.to_f.round(4))
end
end
end

Why This Approach Works 🎯

  1. Value objects provide a clean interface
  2. Empty arrays as fallbacks prevent nil checking
  3. Response parsing is encapsulated
  4. Rate rounding handles floating-point precision

Database Integration

We need to store our currency data. Here’s our migration:

class CreateCurrencies < ActiveRecord::Migration[7.2]
def change
create_table :currencies do |t|
t.string :code, null: false
t.decimal :amount, precision: 14, scale: 4, default: 1.0
t.string :name, null: false
t.datetime :converted_at
t.datetime :deleted_at

t.timestamps
end

add_index :currencies, :code, unique: true
add_index :currencies, :name
add_index :currencies, :deleted_at
end
end

Model Implementation

class Currency < ApplicationRecord
acts_as_paranoid # Soft deletes

has_many :accounts, dependent: :nullify

validates :name, presence: true
validates :code, presence: true, uniqueness: true
validates :amount, presence: true,
numericality: { greater_than_or_equal_to: 0.0 }
end

Automated Rate Updates

Here’s where it gets interesting. We’ll use a background job to update rates:

class UpdateCurrencyRatesJob < ApplicationJob
def perform
Currencies::UpdateRatesService.call
end
end

The actual update service:

module Currencies
class UpdateRatesService < ApplicationService
def call
CurrencyExchange.live.each do |conversion|
update_rate(conversion)
end
end

private

def update_rate(conversion)
Currency.find_by(code: conversion.to)&.update(
amount: conversion.rate,
converted_at: Time.current
)
end
end
end

Scheduling Updates

Configure your scheduler in config/recurring.yml:

staging:
update_currency_rates:
class: UpdateCurrencyRatesJob
queue: background
schedule: '0 1 */2 * *' # Every 2 days at 1 AM

Testing Strategy

Here’s how I test this setup:

RSpec.describe CurrencyExchange do
describe '.list' do
it 'returns structured currency data' do
VCR.use_cassette('currency_layer_list') do # https://github.com/vcr/vcr
currencies = described_class.list
expect(currencies).to all(be_a(described_class::GlobalCurrency))
end
end

it 'handles API failures gracefully' do
allow(described_class).to receive(:get).and_return(
double(success?: false)
)
expect(described_class.list).to eq([])
end
end
end

Model Tests

RSpec.describe Currency do
describe 'validations' do
it 'requires a valid exchange rate' do
currency = build(:currency, amount: -1)
expect(currency).not_to be_valid
end

it 'enforces unique currency codes' do
create(:currency, code: 'USD')
duplicate = build(:currency, code: 'USD')
expect(duplicate).not_to be_valid
end
end
end

Usage in Your Application

Here’s how you’d use this in your app:

# Fetch available currencies
currencies = CurrencyExchange.list
puts "Available currencies: #{currencies.map(&:code).join(', ')}"

# Get current rates
rates = CurrencyExchange.live
rates.each do |conversion|
puts "1 #{conversion.from} = #{conversion.rate} #{conversion.to}"
end

Common Pitfalls to Avoid ⚠️

  1. Don’t store sensitive API keys in your codebase. Use Rails credentials:
EDITOR="code --wait" bin/rails credentials:edit
  1. Don’t update rates synchronously during user requests. Always use background jobs.
  2. Don’t forget to handle API rate limits. Currency Layer has different limits for different plans.

Production Considerations 🚀

  1. Error Monitoring: Add Sentry or similar error tracking:
Sentry.capture_exception(e) if defined?(Sentry)

2. Rate Limiting: Implement exponential backoff for API failures:

def with_retry
retries ||= 0
yield
rescue StandardError => e
retry if (retries += 1) < 3
raise e
end

3. Logging: Add structured logging for debugging:

Rails.logger.info(
event: 'currency_rate_update',
currency: conversion.to,
rate: conversion.rate
)

Remember: Currency exchange rates are critical financial data. Always validate your implementation thoroughly and consider using paid API tiers for production use.

Originally published at https://sulmanweb.com.

--

--

Written by Sulman Baig

Senior Software Engineer with 11 years in Ruby on Rails & Vue.js, specializing in health, e-commerce, staffing, and transport; skilled in software development.