Skip to content

A local proxy for getting Currency Exchange Rates. Built with principled manner using Scala, tagless-final, and Typelevel stacks.

Notifications You must be signed in to change notification settings

arinal/forex-mtl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

25 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

A local proxy for Forex rates

Build a local proxy for getting Currency Exchange Rates

Requirements

Forex is a simple application that acts as a local proxy for getting exchange rates. It's a service that can be consumed by other internal services to get the exchange rate between a set of currencies, so they don't have to worry about the specifics of third-party providers.

An internal user of the application should be able to ask for an exchange rate between 2 given currencies, and get back a rate that is no older than 5 minutes old. The application should support at least 10.000 requests per day.

In practice, this should require the following 2 points:

  1. Create a live interpreter for the oneframe service. This should consume the one-frame API.
  2. Adapt the rates processes (if necessary) to make sure you cover the requirements of the use case, and work around possible limitations of the third-party provider.
  3. Make sure the service's own API gets updated to reflect the changes made in points 1 & 2.

Getting started

Build and publish the docker image of this project.

sbt docker:publishLocal

Once the image is published, start all of the required images.

docker-compose up

Try to get the conversion rate between Indonesian Rupiah and Japanese Yen.

curl 'localhost:9090/rates?from=IDR&to=JPY'

What about, asking for all of the possible permutations of exchange rates?

curl 'localhost:9090/rates'

Firing up unit testing and integration testing.

sbt test
sbt it:test # make sure docker-compose up has executed beforehand

Happy hacking!

Technology used

Approach

oneframe service supports multiple pairs of queries in one GET request. Instead of asking for only one exchange rate, we can also ask for other rates at once like GBP to USD, JPY to AUD, etc. To get the most benefits out of this, Forex will literally take every permutation of our supported currencies and caches all the rate results taken from oneframe. Of course, this will only work if our supported currencies are minimal. Querying all of 22350 currency combinations in the world in a single GET to oneframe doesn't sound like a good plan, but given that our server only supports 14 currencies, the permutation is only 182 and luckily still in the acceptable range of the oneframe server.

The main goals of Forex are two-fold:

  • Overcome the limitations of 1000 invocations per day that the oneframe server gives.
  • If local cache is used, it must be no older than 5 minutes.

If we call oneframe every 86.4 seconds starting early in the day, the 1000th call will be at the very end of the day. That is, Forex tries to call oneframe 1000 times in a day by waiting 86.4 seconds between each calls, hence the cache age wouldn't be older than 86.4 seconds. And yes, we call oneframe greedily to update every currency combinations within each call :)

The scheduler to update the cache is implemented using fs2 here.

Code practices and structures

The initiator of this project used typelevel stacks and aimed to be more using scala in functional way. Aligned with this initiative, this project tries to follow functional programming principles by avoiding side effects and impurity. Every impure expression will be wrapped inside an IO construct.

Forex structures the packages based on hexagonal architecture.

                               +------+
                               | boot | (boot knows everything, arrows are not shown)
                               +------+

                      +----------+   +------------+
                      | app/http |   | app/stream |
                      +---+------+   +--+---------+
                          |             |     
                          |     +-------+
                          |     |
   +--------------+  +----v-----v---+ +------------------+  +---------------+
   | interps/http |  | app/programs | | interps/inmemory |  | interps/dummy |
   +-----------+--+  +------------+-+ +--------+---------+  +-------+-------+
               |                  |            |                    | 
               |                  |            |                    | 
               |                +-v----+       |                    | 
               +--------------->| core |<------+--------------------+
                                +------+   
  • core is the business logic. This layer must not depend on any infrastructure logic (Kafka, Cassandra, etc). Every aggregate root in core has an algebra which will be implemented in the interpreter layer. Since our domain is small, the only algebra core exposed is this. Examples of business logic are: generating new employee code, validating items, as long as it doesn't require infrastructure involvements.
  • interps or interpreter, or sometimes also known as infrastructure layer from DDD world, implements the algebra exposed by core. For example, dummy implementation generates a dummy rate which might be handy for unit testing, but interps/http contains a HTTP client to get the rate from another server.
  • programs acts as an intermediary between app and core by re-exposing another algebra. Internally it calls core's algebra. Please note that we shouldn't write business logic here.
  • app is the application layer. Forex has two app, one is for updating the cache every 86.4 seconds, and the other one is for serving user rate queries.
  • boot is the highest layer and knows everything underneath it. Since components are modularized, boot will wire them together and start the application.
  • commons is everything that doesn't contain Forex specific logic, and can be pretty much reusable on other projects. Note that commons is not even under the forex package.

Room for improvements

  • The discussed approach is only working if the supported currencies are minimal.
  • Retry logic when calling oneframe API.
  • More testing, or if possible, apply testing based on algebraic laws.
  • OpenAPI specifications, probably using tAPIr.
  • Streamify the initialization process. Currently we call .compile.drain twice. We can simplify it to become one.
  • Simulation testing will verify that Forex never exceeds 1000 invocations (a oneframe constraint). A fake full day test ought to be implemented by supplying fake timer.

Thank you note

A huge thank you to the initiator of this project, the shape has already been good since the very beginning. This project is only a continuation from a good foundation.

A huge thank you to Olivier Mélois, author of weaver-test.

About

A local proxy for getting Currency Exchange Rates. Built with principled manner using Scala, tagless-final, and Typelevel stacks.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages