Building a tracker for gym class sign-up using the AWS Serverless Application Model (SAM)

I have written a tracker running on AWS to get notified about free spots in my favourite gym class. I used AWS Lambda, EventBridge Scheduler, DynamoDB and SNS. In this article I describe the setup and architecture of my gym class tracker.

Iris Hunkeler
AWS Tip

--

Over Easter, many people look forward to visiting family. While I of course also had some family time planned, I was additionally intending to spend some time in a gym class I somewhat regularly visit. So you can imagine my disappointment, when I realized that the class I was planning to visit was unfortunately booked out! Instead of backing down, I decided I wanted to figure out if my software engineering skills can solve that problem and help me secure a spot.

The Problem

My gym requires online sign-up for classes. Sign-up starts 24 hours before the class and places are limited. When all places are taken, sign-up is no longer possible. However, if anyone cancels their spot, it becomes available again!

I had missed the original sign-up and the class was booked out. However, I assumed some people would cancel later on — this often seems to happen. But I did not want to keep checking the website every few minutes. Instead, I preferred to be notified, if (when!) a spot became available again. Basically, instead of having to actively pull the information about free spots I wanted to get a push system.

So in short, I wanted a system as shown below: I wanted to set up tracking for my gym class and get an email if I could go and sign up for it.

C4 architecture model — context diagram for gym class tracker

(If you’re not familiar with that type of architecture diagram, you can read more about the C4 architecture model.)

Proof-of-concept

I checked my gym’s website and realized they use a REST endpoint to send the course information to their frontend. Perfect, I can work with that!

I quickly assembled a very simple proof-of-concept on AWS: I set up a Lambda function written in Python which would execute a POST call against the gym’s REST endpoint (with the body I copied from their website). I used an Event Bridge Scheduler to trigger that Lambda every minute. And if the call to the gym’s API would finally show that there is a free spot, a notification would be sent to my email address using an SNS Topic I manually set up.

Here is the lambda function I used for the PoC: https://github.com/iris-hunkeler/gym-class-tracker/blob/main/proof-of-concept/lambda_function.py

That setup was of course very flawed. Its biggest issue was probably the missing state: If the course became bookable, my gym class tracker PoC would now send out emails every minute about that. So when I went to bed that night, I was a bit scared that I would wake up to 300 emails in case somebody canceled their spot in the middle of the night.

I was a bit disappointed the next morning, that there was still no spot available. Apparently — based on email I got — for just 2 minutes in the middle of the night, the course was bookable?! Well, I just had to continue waiting.

And it actually worked out! Shortly before the class started, somebody canceled and I could book my spot.

From Proof-of-Concept to MVP

Inspired by the successful use of my simple proof-of-concept, I decided to improve and stabilize it. The biggest flaws I saw were:

  • All AWS resources were currently created and handled manually. I wanted to switch to Infrastructure-as-Code that could be deployed using the CLI.
  • The gym class to be tracked was currently hard-coded into the Lambda function. I wanted my gym class tracker to be a bit more flexible and move that information into a DynamoDB table.
  • There was absolutely no state in the PoC, which would lead to very spammy behavior in case a spot in a tracked class becomes available. I wanted to make sure that I only get notified on state changes so the system would need to store the last state into the already planned DynamoDB table.

Setup Infrastructure-as-Code and Deployment from VS Code

I decided to use the AWS Serverless Application Model (SAM) which is an extension of CloudFormation and built to simplify the building, testing and deploying of serverless applications on AWS. In order to deploy my application directly from VS Code, I used the AWS Toolkit for Visual Studio Code. This allowed me to define all required AWS resources in a template.yaml (including the required permissions for the resources to collaborate) and deploy using the SAM CLI directly from VS Code. Very neat, I was happy with this setup!

Architecture

To address the other two issues, I reworked my architecture to include a DynamoDB table which would hold all information for separate “tracker queries” for gym classes. To set up a tracking, an entry would need to be created in this table.

AWS architecture overview

The lambda function reads all currently active tracker queries (so compared to the PoC there are multiple simultaneous queries supported now), calls the gym’s API and updates the state in the table. Every status update (or error) triggers a notification.

I distinguish two different types of state:

  • active-status (true/false): a tracker query would automatically deactivate if the time of the class has passed , since it is now not relevant anymore.
  • availability-status (Bookable, Not Bookable or Unknown for new queries): The latest information about if a class is bookable or not.

I deployed this setup and managed to successfully validate it with a few gym classes I tracked just for testing.

The current state of the project can be found on Github: https://github.com/iris-hunkeler/gym-class-tracker

Issues along the way

AWS nuisances

I really enjoyed using AWS SAM together with the Toolkit. It made testing, debugging and deployment of my lambda function really easy.

Nevertheless, I did stumble across some technical nuisances during development. For example, I learned that !Ref [some AWS resource] returns different values for different resources.

To make sure my Lambda function has the permissions it requires, policies can be added to it. That worked easily enough for my DynamoDB table, I could use !Ref GymClassTrackerTable to reference the name of my table.

- DynamoDBCrudPolicy:
TableName: !Ref GymClassTrackerTable

But then I struggled to properly add the policy for my lambda function to access the SNS topic, until I figured out that !Ref GymSNSTopic does not return the topic name but the topic ARN. So instead I had to explicitly get the topic name using !GetAtt GymSNSTopic.TopicName

- SNSPublishMessagePolicy:
TopicName: !GetAtt GymSNSTopic.TopicName

Small inconsistencies like this can make AWS a bit painful to work with.

Quick increase in complexity

I work as a backend engineer. So finding elegant solutions and abstractions to business problems is what I do all day. My number one question is probably “But what should happen if [enter strange edge case]?”. And I was still surprised by how fast the complexity of this simple application grew. I wrote the whole Lambda function in a single file, only structured through methods. And soon I found myself adding if condition after if condition to handle unexpected cases. In addition to the growing complexity, the missing abstractions made my code absolutely untestable.

Writing this simple gym class tracker gave me a strong reminder of why we use abstractions and proper clean code practices in production code.

Next Steps

Of course, the current setup is still extremely simple. There is no comfortable way to set up a new tracker query (it currently has to be created manually in the DynamoDB table), and the email address for notifications needs to be set up at deployment time. So the next possible improvement steps are probably setting up an additional Lambda function with an API gateway to setup new tracker queries which also allows to specify an email address for the notifications. Additionally, properly refactoring the Lambda function and adding abstractions to deal with DynamoDB, SNS and the gym API is definitely required. This also finally enables the writing of reasonable unit tests.

Conclusion

I had fun solving a small problem I encountered in my private life using my software engineering skill set. I felt comfortable applying my previous AWS and Python knowledge. I was positively surprisedby how quickly a simple, but running PoC could be developed and deployed. But afterwards moving from PoC to (a still very simple) MVP with a decent deployment setup took more effort than I expected.

Invested time:

  • PoC: ~2 hours
  • Getting from PoC to MVP: ~2 days
  • Manual verification, limited clean-up and documentation: ~1 day

Using AWS SAM together with the Toolkit was comfortable and enabled quick prototyping. Additionally, using only serverless resources, I could build my application without having to worry about planning resources and commissioning servers.

I’m now perfectly set up to never miss my gym class again, since I can always just use my gym-class-tracker to get notified about free spots. But setting an alarm for the time when sign-ups start might probably still be easier :)

Iris Hunkeler is a passionate Software Engineer based in Zurich (Switzerland). She has worked for clients in various industries such as public administration, e-commerce or banking. Her main interests are backend development, cloud and DevOps. She is motivated by finding achievable and elegant solutions to complex problems to help her clients reach their individual goals.

You can follow her on Medium or LinkedIn if you want to be notified about new articles.

--

--

Writer for

Software Engineer | IT Consultant | Speaker | passionate about Kotlin/Java, cloud (AWS), DevOps and agile work | encourages more women & diversity in tech