- Ruby 3.x installed
- Bundler installed (
gem install bundlerif needed)
From the project root, run:
bundle installRun the Sinatra app with:
ruby app.rbThe app starts on http://localhost:4567 by default.
Use any bearer token value while testing locally.
curl -X GET http://localhost:4567/vault/items \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X POST http://localhost:4567/vault \
-H "Authorization: Bearer YOUR_TOKEN"
curl -X PUT http://localhost:4567/vault/items/2 \
-H "Authorization: Bearer YOUR_TOKEN"I used Copilot to help scaffold the initial Sinatra and middleware setup so I could focus on the core rate-limiting logic. That included a basic Hello World endpoint and the middleware wiring, and I built the rate limiter from there.
The assumption I had was that token verification would happen before rate limiting and was out of scope.
I skipped the token verification for this assessment due to the time constraint. Currently, I'm checking to see if there is an authorization header and grabbing the token. This should be handled in its own class, and include verifying that the token is under a certain character size limit and that it restricts the special characters allowed.
The secondary issue from this is if there is no token, then a rate limit error would be applied to anyone who is trying to hit this endpoint without a token, when it should be a 401
Currently, the rates are handled with static values tied to the method. This works since there's one endpoint per method type.
Instead, if I had more time I would set them up as config values, and create an app config class to retrieve the limits.
3 hours
From the start, I knew I needed the rate limiter to be its own class with also injecting the store. This would allow me to test the class in isolation while being able to set the store up for specific test cases.
Having the filtering be its own method allowed me to test both parts of the problem separately. This allowed me to have a simple table driven test for filtering to cover old timestamps being removed, while having a more feature driven describe block for the call method.
I would also spend more time working on edge cases and integration test
Most of my time was focusing on the rate limiter's main functionality. This resulted in me hard coding the rates and using the request methods as the lookups instead of including the paths. This only worked since each endpoint has its own request method, but it would fail as soon as more endpoints were added. I was also limited in the number of tests I could write. If given a few more hours, I would have written a test for each endpoint and the combination of endpoints used. I would have also been sure to make an integration test to test the rate limiter out of isolation
- I used Copilot to scaffold the initial Sinatra and middleware setup so I could focus on the rate-limiting logic.
- I kept the rate configuration hard-coded instead of building a separate config layer.
- I focused test coverage on the core middleware behavior and filtering logic rather than building broader integration coverage.
The trickiest part was choosing an algorithm that stayed accurate within the time limit. Initially, I thought of using a counter based approach to avoid massive arrays for memory efficiency but I it bacem hard to reduce the couter based on time stamps. The sliding window is a common leet code algoritm that I thought applied well here but I am concerned with with the scaling memory usage when theres 1000's of clients with 1000's of request.