This article details GitHub's engineering process to redesign their star history API, transforming it from an inefficient identity-based endpoint to a performant, aggregate-based solution. The key architectural shift involved moving from paginating through individual stargazer records to pre-filtering by time range before aggregation, significantly reducing database load and improving response times. This case study highlights critical lessons in API design, performance optimization, and understanding user needs versus existing data models.
Read original on GitHub EngineeringInitially, GitHub's API for retrieving repository star history, GET /repos/{owner}/{repo}/stargazers, was designed to answer "who starred this repository?" It returned a paginated list of individual users, each with an associated `starred_at` timestamp. This design had two major flaws:
After restricting access to the original endpoint due to privacy concerns, GitHub engaged with maintainers and tool builders. They discovered that users primarily needed the curve – an approximate signal of interest and growth over time – not individual identities. This crucial insight allowed them to decouple the historical trend data from user identity, enabling a more efficient, purpose-built API.
Design Lesson: Separate Data Concerns
Do not let an existing data model dictate how a new use case is served if that model introduces inefficiencies or privacy risks. Clearly identify the core data requirement for the new use case and design an API around it, even if it means transforming or aggregating existing data.
Several approaches were considered to address the performance and privacy issues, each with its own trade-offs:
/* Inefficient: Aggregates all stars before pagination */
SELECT DATE(created_at), COUNT(*)
FROM stars
WHERE repository_id = ?
GROUP BY DATE(created_at)
ORDER BY DATE(created_at)
LIMIT ? OFFSET ?;
/* Efficient: Filters by time range first */
SELECT DATE(created_at), COUNT(*)
FROM stars
WHERE repository_id = ? AND created_at BETWEEN ? AND ?
GROUP BY DATE(created_at)
ORDER BY DATE(created_at);This design allows one request to cover up to 30 weeks of history, drastically reducing the number of requests (e.g., from 2,511 to 24 for `react/react`) and the database load. While exceptionally dense weeks remain a scaling limitation, the overall system is significantly more efficient and safer.