Skip to content

Paidy Forex Proxy Coding Exercises#78

Open
Hibar3 wants to merge 11 commits intopaidy:masterfrom
Hibar3:feat/oneframe-service-integration
Open

Paidy Forex Proxy Coding Exercises#78
Hibar3 wants to merge 11 commits intopaidy:masterfrom
Hibar3:feat/oneframe-service-integration

Conversation

@Hibar3
Copy link

@Hibar3 Hibar3 commented Jan 28, 2026

Overview

This PR implements a robust local proxy for the Forex currency exchange rate service. It addresses the 1,000 requests/day limitation of the One-Frame upstream API to support 10,000+ requests/day while maintaining a 5-minute data freshness guarantee.

Key Requirements Addressed

  • Currency Support: Returns exchange rates for 2 supported currencies.
  • Data Freshness with intelligent caching: Rates are never older than 5 minutes, enforced via TTL validation.
  • High Throughput: Supports 10,000+ requests/day using a single API token by minimizing calls to the One-Frame API.
  • Graceful Error Handling: Validate and handle errors gracefully with clear and descriptive messages for invalid currencies or upstream failures

Technical Implementation

  • Caching Layer: Implemented a thread-safe InMemoryCache.scala with TTL support.
  • Freshness Validation: Integrated logic in OneFrameLive.scala to verify timestamps against the 5-minute window.
  • Error Handling: Added specific error types like RateStale and improved descriptive feedback for upstream failures.
  • Configuration: Unified TTL settings in application.conf.

API Endpoints

  • GET: http://localhost:3000/rates?from=USD&to=JPY - for getting currency exchange rates
  • POST: http://localhost:3000/rates (add-on) - for users to set the amount they want to convert

Testing

  • Unit Tests: Added unit test for cache and rates interpreter.
  • Manual Test: Verify via curl "http://localhost:3000/rates?from=USD&to=JPY".

Comment on lines +6 to +7
bid: Price,
ask: Price,
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added bid and ask fields to better match with response from OneFrame

Comment on lines +13 to +15
ParseFailure(
s"Invalid currency: '$s'",
s"Currency must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added ParseFailure to query param decoder to check if currency is valid, returns error message otherwise

Comment on lines +41 to +79
// Validate 'from' parameter
queryParams.get("from") match {
case Some(values) if values.nonEmpty =>
val fromValue = values.head
if (fromOpt.isEmpty) {
errors += s"Invalid 'from' currency: '$fromValue'. Must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD"
}
case _ => errors += "Missing required query parameter: 'from'"
}

// Validate 'to' parameter
queryParams.get("to") match {
case Some(values) if values.nonEmpty =>
val toValue = values.head
if (toOpt.isEmpty) {
errors += s"Invalid 'to' currency: '$toValue'. Must be one of: AUD, CAD, CHF, EUR, GBP, NZD, JPY, SGD, USD"
}
case _ => errors += "Missing required query parameter: 'to'"
}

// If there are validation errors, return BadRequest
if (errors.nonEmpty) {
BadRequest(ErrorResponse(errors.mkString("; ")))
} else {
// Both parameters are valid, proceed with the request
(fromOpt, toOpt) match {
case (Some((from, _)), Some((to, _))) =>
rates
.get(RatesProgramProtocol.GetRatesRequest(from, to))
.flatMap {
case Right(rate) => Ok(rate.asGetApiResponse)
case Left(error) => mapProgramError(error)
}
case _ =>
BadRequest(
ErrorResponse(
"Missing required query parameters. Both 'from' and 'to' parameters are required. " +
"Example: /rates?from=USD&to=EUR"
)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

validation to ensure only supported currencies are parsed to parameter. Also checks for missing or empty parameters

Comment on lines +58 to +59
implicit val currencyDecoder: Decoder[Currency] =
Decoder.decodeString.emap { s =>
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added custom Decoder that validates currency strings against supported values in One Frame

Comment on lines +84 to +89
// POST /rates
case req @ POST -> Root =>
req
.as[PostApiRequest]
.flatMap { request =>
rates
Copy link
Author

@Hibar3 Hibar3 Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an additional POST API endpoint that accepts user input to set the amount they want to convert; http://localhost:{port}/rates

Sample request Body;
{ "from": "JPY", "to": "USD", "amount": 1 }

Response:
{ "from": "JPY", "to": "USD", "amount": 1, "exchangeRate": 0.15538797046424008, "convertedAmount": 0.15538797046424008, "timestamp": "2026-01-29T15:08:35.341Z" }

override def get(request: Protocol.GetRatesRequest): F[Error Either Rate] =
EitherT(ratesService.get(Rate.Pair(request.from, request.to))).leftMap(toProgramError(_)).value

override def compare(request: Protocol.CompareRatesRequest): F[Error Either Rate] =
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for comparing rates from 2 different currency pairs using the from and to fields

Comment on lines +9 to +13
class InMemoryCache[F[_]: Sync] private (
cache: ConcurrentHashMap[Rate.Pair, CacheEntry]
) extends Algebra[F] {

override def get(pair: Rate.Pair): F[Option[Rate]] = {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is a simple in-memory cache for rates. it stores rates in a concurrent hash map and checks for expiration when retrieving rates using a specified TTL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant