Skip to content
Rathaworks

EngineeringWhy a social app's feed should never touch its payments database

A creator app has social-media read traffic and a payments system's correctness requirements. Here's how I separated the two without reaching for microservices.

2 min read

QuickBite is a food discovery app where creators post reels, people claim restaurant offers, and restaurants pay creators for the visits they drive. That makes it two systems wearing one coat: a social app that is read thousands of times for every write, and a money system where every rupee must be accounted for.

Reads and writes want different databases

A feed scroll that fans out into a dozen joined queries per screen will take a relational database down long before growth becomes a good problem. But the ledger — offers redeemed, creator earnings, payouts — needs transactions, constraints and foreign keys. Those requirements pull in opposite directions, so I stopped asking one store to satisfy both.

  • PostgreSQL (with PostGIS) is the system of record and the only home of money. Every write lands here first.
  • MongoDB holds read models — feed cards, counters, inboxes, saves — each shaped for one screen and indexed for one query.
  • Redis handles hot, short-lived state: rate limits, sessions, idempotency keys.

The outbox: how a write reaches the read side

Dual writes — “save to Postgres, then update MongoDB” — eventually leave the two out of sync when the second call fails. Instead, the write and an event describing it are committed in the same transaction. A worker then publishes outbox events to the read models, safely in parallel:

sql
-- inside the same transaction as the business write
INSERT INTO outbox (aggregate, event, payload) VALUES ('post', 'post.published', $1);

-- worker loop
SELECT id, event, payload FROM outbox
WHERE processed_at IS NULL
ORDER BY id
LIMIT 100
FOR UPDATE SKIP LOCKED;
Workers claim events without stepping on each other.

The read side is eventually consistent — usually by well under a second — and the design says so explicitly, so screens can be built around it rather than surprised by it.

Cursor pagination, everywhere it scrolls

OFFSET 50000 gets slower linearly, and on an infinite feed it duplicates and skips posts as new ones arrive. Feeds, comments and search use a cursor — the last item's timestamp and id — instead:

sql
SELECT * FROM posts
WHERE (created_at, id) < ($1, $2)
ORDER BY created_at DESC, id DESC
LIMIT 20;

A modular monolith, not microservices

Five modules — users, posts, restaurants, analytics and admin — live in one deployable API with strict boundaries: no module reaches into another's tables. There's no network hop between them and no distributed transaction to debug, yet any module can be extracted into its own service later without a rewrite. For an early product that's the right side of the trade.

Trust and observability before launch

  • OpenTelemetry traces and Prometheus metrics on every request, with alerts — you can't fix what you can't see.
  • Search that understands how people actually type: phonetic matching across Indic scripts and synonyms mined from search logs.
  • Abuse controls designed in from the start — duplicate-content detection, velocity limits and moderation — because reward-driven apps attract abuse the day rewards go live.
  • Architecture
  • PostgreSQL
  • MongoDB
  • Scalability
Rasmiranjan Sahoo, freelance web and mobile app developer based in Bhubaneswar, India

Written by Rasmiranjan Sahoo

Full-stack and React Native developer. I design and build websites, apps and business software end to end — and write about what I learn shipping them. More about me

Let's build the thing your business is missing.

Tell me what you need in two minutes. You'll hear back within 24 hours, and get a fixed-price proposal within 48 hours of our call.

  • Reply within 24 hours
  • Fixed quote in 48 hours
  • NDA on request