Introduction
PRESTO is the fare system for public transportation in the Greater Toronto Area. This system not only contains the city of Toronto but also other regional and city transit systems in the cities next to Toronto. Some of them are suburbs of Toronto whereas others are independent cities by themselves. In this post, we are trying to design the PRESTO ticket fare system.
While the annual number of usage counts in all the networks is approximately 41 million, not all of them use the PRESTO system. Some of them use Credit Cards/Debit Cards, day ticket, weekend ticket, cash, etc. However, for the sake of simplicity we will assume that all of them use the PRESTO system.
Workflow
The way this system works is a rider purchases a PRESTO card from one of the authorized vendors or the vending machines. Each PRESTO card has a number assigned to it to uniquely identify the card. The user can then login into the PRESTO app on their mobile or web and link that account with the card. A PRESTO account can have multiple cards linked to it. However, each PRESTO card can be linked to only one account. So there exists a one to many relationship between the user account and PRESTO card whereas a card to account relationship is one-to-one.
After linking these cards to their accounts, the users can use either credit card or debit card or their bank account to their account. This data needs to be securely saved for auto-pay. The user can "recharge" their card with a certain amount and can use the different transportation services by tapping their PRESTO card to a terminal device at each bus stop, train station, etc. Once the user taps this card, a certain amount is deducted which is valid for 2 hours. The amount deducted differs from one transportation network to another. For example, for TTC, it is $3.30 whereas for UP it is $9. This fare is valid until 2 hours. One exception to this rule are GO Transit and UP which do not depend on the duration. So the user needs to tap the card at both the source and destination to mark a trip and be charged accordingly.
Functional Requirements
The functional requirements for the PRESTO card should be:
- User registration.
- Linking PRESTO Card number with a user account.
- "Tap" functionality, which calculates the amount to be charged.
- The app should let the user check their current balance.
- Fare inspector verification to check if the current PRESTO has an active trip.
Non-functional Requirements
The non-functional requirements for this system are:
- High Availability: The system needs to be highly available. We are aiming for 99.5% availability duration.
- Scalability: The system should easily scale up during peak traffic hours (office commute time), holidays or during special events because the number of user registrations, card linking requests and taps will go up significantly during those times.
- Eventual Consistency can be tolerated because once the card is tapped, the local balance will be directly deducted but the balance can be updated later. We can have a periodic sync of the card balance with the server. Since only the first tap is considered in a two hour duration or two taps for GO and UP, we can update the data eventually in few minutes instead of instantaneously.
Back-of-the-envelope Estimates
- We know that the number of taps in 2024 was 41 million. Considering an increase of 2 million people per year, we are looking at 51 million taps in 5 years. So the total number of taps in 5 years is 43 + 45 + 47 + 49 + 51 = 237 million, which is an average of 47.5 million tap requests, which is 47.5 * (10 ^ 6) requests per year.
This means the number of requests per second is ~55 requests per second.
- Assume 100 new card registrations per day, which is a very small number per second.
- The number of new user registrations is lesser than that. So we can assume 60 requests per second to the server.
- Therefore, the number of servers needed is not a significant number.
Low-level Design
APIs:
POST /api/user
POST /api/card/{cardNumber}/{userId}
POST /api/card/{cardNumber}/tap/{terminalId}
GET /api/card/{cardNumber}/balance
GET /api/card/{cardNumber}/inspector/{inspectorId}
High-level Design
As stated earlier, the number of servers needed is not significant. But the number of servers needed could increase during peak hours or special events. Scaling up would mean adding new servers. This can happen during the times when the traffic is high. Since we need to dynamically change the number of servers based on the traffic, it would be good to add a load balancer.
The service layer can be broken into microservices, each handling a different aspect of the system such as the User service, Tap service, and the Card service. Since the scaling needs of each of these components is different, splitting them into microservices will make it easier to scale them up or down independently.
Also, it would make sense to have these services deployed in multiple pods and also in different regions. This is so that if one server were to go down, a server in a different region could take over and serve the incoming requests.
Since eventual consistency is what is needed, we can use a non-relational database such as MongoDB or Cassandra. This would ensure availability and scalability of data. This would also ensure that no data is lost. However, we need to add logic to calculate the balance separately. This would also keep the system simple.
Top comments (0)