MMamun
All work

Case study · 2025

Fanfare: a social platform's backend

Built and maintained the GraphQL backend behind a consumer social app: feeds, profiles, comments and real-time notifications.

01

Context

Fanfare is a consumer social product for Android. Users create profiles, post to a shared feed, comment, and get notified when others interact with them. I worked on the platform's backend and its internal admin console, owning the GraphQL API and the data layer that fed both the mobile app and the operations team.

02

Problem

A social feed is deceptively hard: reads massively outnumber writes, every screen stitches together data from several places (the user, their graph, the content, the interaction counts), and notifications have to feel instant. The early API made a round-trip per piece of that, so busy screens fanned out into many slow queries and the feed felt sluggish as content grew.

03

Constraints

  • Mobile-first: the client is on flaky mobile networks, so the API had to return exactly what a screen needs in one request, with no chatty round-trips.
  • Mixed data shapes: relational data (users, the social graph) and document/feed data don't fit one store cleanly.
  • Small team, live product, so changes had to ship incrementally without a big-bang rewrite or downtime.
04

Approach

GraphQL as the single mobile contract

Modelled the API in NestJS with GraphQL so each screen fetches its whole tree in one request. Resolvers are split by domain (profile, feed, social, notifications) with DataLoader batching so a feed of N posts doesn't become N+1 queries against the database.

Right store for each shape

Kept strongly-relational data (accounts, the follow graph) in PostgreSQL where joins and constraints belong, and content/feed documents in MongoDB where the shape is flexible and read-optimised. Each store does what it's good at instead of forcing one to do both.

Redis where reads hurt

Put hot, read-heavy data behind Redis (session/auth state and expensive aggregate counts like likes, comments and unread badges), with invalidation on the write path. That took repeated pressure off Postgres/Mongo for the values every screen reads.

Real-time notifications via Firebase

Wired interaction events (comment, reaction, follow) into Firebase Cloud Messaging so pushes land immediately, keeping the notification path off the request/response cycle.

Made the hot paths fast, and kept them safe

Profiled the heaviest resolvers and refactored them, which roughly halved their compute time and cut memory use by about a third. I also kept high unit-test coverage on the API layer, which held post-deployment bugs down as the surface grew.

05

Architecture

Client

Android app

Service

NestJS GraphQL API

Data

Redis cache
PostgreSQL
MongoDB

External

Firebase FCM
  • Android appNestJS GraphQL API· 1 query per screen
  • NestJS GraphQL APIRedis cache· hot reads / counts
  • NestJS GraphQL APIPostgreSQL· users · graph
  • NestJS GraphQL APIMongoDB· feed · content
  • NestJS GraphQL APIFirebase FCM· push events
Android client talks to a single NestJS GraphQL API; the API batches through DataLoader, reads hot values from Redis, and persists to Postgres (relational) and Mongo (content). Interaction events fan out to FCM for push.
06

Outcome

  • Doubled API throughput and cut latency by ~45% after reworking the GraphQL schema and resolver logic.
  • Cut the data sent per request by ~60% by tightening GraphQL selections and REST payloads.
  • Added role-based access control and an e-commerce checkout flow, and gave the ops team a matching admin console for user management, moderation and usage.
07

What I'd change

I'd invest earlier in load and query observability. A lot of the tuning was reactive; we found the slow screens by using the app. With per-resolver timing and slow-query logging from day one, we'd have caught the N+1 and cache-miss hotspots before users felt them. I'd also formalise cache invalidation as an event the write path emits, rather than something each mutation remembers to do.