•CASE STUDY

Simple Greeting-Card Web App (MVP Architecture)

4 min read·757 words·Beginner

Asked at

1 candidate report in Feb 2026

How to use this case study

SDE-2 / Mid

  • Design a minimal architecture (frontend, backend API, database, object storage) and the data model for templates
  • Cards and recipients

SDE-3 / Senior

  • Explain the send flow (email with a secure link)
  • Image uploads with pre-signed URLs
  • Basic security (unguessable links, rate limits)

Staff / Principal

Explain how the MVP evolves as usage grows (CDN, queues for email, scheduled sends, analytics) without over-engineering day one

Problem RestatementProblem

Atlassian asked a deliberately simple question: design a minimal web app for greeting cards. A user picks a template (birthday, thank you), customizes the message and maybe adds a photo, then sends it to a recipient by email. The recipient opens a link and sees the card. The interviewer wants a clean MVP (minimum viable product) architecture, not a massive distributed system, plus a sense of how it would grow.

RequirementsRequirements

  • Browse templates. Create a card (template + message + optional image).
  • Send to one or more recipients by email (now, or scheduled).
  • The recipient views the card via a unique link, with no account needed.
  • The sender sees whether the card was opened.

1.1 Scale (MVP)

  • Thousands of cards per day, spiking around holidays. One region is fine.

MVP ArchitectureArchitecture

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
    B["Browser - React SPA"] --> CDN["CDN - static app + template images"]
    B --> API["Backend API - one service"]
    API --> DB[("Postgres")]
    API --> OS[("Object storage - uploaded photos")]
    API --> MAIL["Email provider - SendGrid / SES"]
    R["Recipient"] -->|"card link"| API
  • Frontend: a single-page app served from a CDN.
  • Backend: one small service (e.g., Node, Python or Java) with a REST API. No microservices needed yet.
  • Database: Postgres for users, templates, cards and recipients.
  • Object storage for user photos, uploaded directly from the browser with a pre-signed URL.
  • Email provider API to send emails (don't run your own mail servers).

Data Model and APIsData model

templates:  template_id, name, category, image_url, layout_json
cards:      card_id, sender_id, template_id, message, photo_key, created_at
recipients: card_id, email, share_token (random, unguessable), sent_at, opened_at, scheduled_for
  • GET /templates
  • POST /cards { template_id, message, photo_key? } → { card_id }
  • POST /uploads → a pre-signed URL for the photo
  • POST /cards/{id}/send { recipients: [...], send_at? }
  • GET /c/{share_token} → the card page (records opened_at)

Send FlowFlows

  1. Create a recipient row with a random share token (e.g., 128-bit, URL-safe). The link is https://cards.example/c/{token}, and it can't be guessed.
  2. Send the email through the provider with the link.
  3. When opened, render the card and set opened_at (only the first time).

Security basics: rate-limit sends per user (to stop spam), validate uploads (type and size), escape the message text (prevent script injection), and let links expire or be revoked if needed.

Deep Dive — Sending the cards on Mother's DayDeep dive

The app is small and its traffic is not: one morning a year produces more sends than the previous three months. Where the email is sent from decides whether that morning works.

Weak

Send inside the request

The API calls the email provider and returns when it accepts.

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  U["Send card"] --> API["API handler"]
  API --> SMTP["Email provider - 300 ms, sometimes seconds"]
  SMTP --> HOLD["Request thread blocked for the whole call"]
  HOLD --> POOL["Holiday spike exhausts the thread pool"]
  SMTP -->|"provider rate limit"| ERR["500 to the user - card lost"]
  ERR --> RETRY["User hits send again - recipient gets two cards"]

The provider's latency and rate limits are now the app's. Worse, a failure loses a card the user believes they sent, and their natural retry produces duplicates.

Good

Put sends on a queue

The API writes the card and enqueues a send job; workers call the provider.

The request is fast, the spike is absorbed by the queue, and a provider hiccup delays delivery instead of failing it. What is undefined is what a worker does when the provider rejects or times out — a naive retry sends the card twice, and the provider's rate limit is still hit by however many workers happen to be running.

Best

Queue, with idempotent sends and a rate budget

Architecture diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
  API["POST /cards/{id}/send"] --> ROW["Row: status queued, send_key = card_id + recipient"]
  ROW --> Q[("Queue")]
  Q --> W["Workers"]
  W --> TB{"Token bucket - the provider's rate limit"}
  TB --> P["Provider - Idempotency-Key: send_key"]
  P -->|"transient failure"| BACK["Backoff and retry - same key, no duplicate"]
  P -->|"permanent failure"| DLQ[("Dead letter - user told it failed")]
  SCH["scheduled_for"] --> SWEEP["Scheduler enqueues due sends every minute"]
  SWEEP --> Q
  • An idempotency key per card and recipient, passed to the provider and stored on the row. A retry — whether the worker's or the user's — resolves to the same send rather than a second email.
  • A shared token bucket sized to the provider's limit, so adding workers increases throughput up to the limit and never past it. Per-worker limits cannot do this.
  • Scheduled sends fall out for free. Store scheduled_for and have a job enqueue due cards each minute — the same path, just entered later, which also spreads the holiday load across the morning.
  • Permanent failures are visible. After the retries, the card is marked failed and the sender is told. A card silently stuck in a queue is the worst outcome for this product.

Everything else about the app can stay simple. This is the one place where the holiday spike, an external dependency and a user who will click twice all meet.

Wrap-UpWrap-up

Start simple: an SPA on a CDN, one backend service, Postgres, object storage for photos (pre-signed uploads) and a managed email provider. Cards get recipient rows with unguessable share tokens, sent by email and tracked on open, with rate limits and input sanitization. Grow step by step: a queue for email, a scheduler for scheduled sends, horizontal scaling and caching, and async rendering.

More Case Studies

Frequently Asked Questions

What is the Simple Greeting-Card Web App (MVP Architecture) system design question?

Simple Greeting-Card Web App (MVP Architecture) is a system design interview question asked at FAANG companies. It covers api design, storage, cdn and tests your ability to design scalable, production-ready systems. InterviewSkool's breakdown walks you through requirements, API design, architecture, and trade-offs.

Which companies ask the Simple Greeting-Card Web App (MVP Architecture) question?

Atlassian have reportedly asked variations of this question in system design interviews. The exact wording may differ, but the core design challenges remain the same.

How should I prepare for the Simple Greeting-Card Web App (MVP Architecture) interview question?

Start with the problem statement and scale estimates, then design the high-level architecture. Focus on the core components, data model, and API design. InterviewSkool's breakdown covers the full solution with mermaid diagrams and trade-off analysis to help you prep efficiently.

What level is the Simple Greeting-Card Web App (MVP Architecture) question?

This question is suitable for SDE-2, SDE-3, and Staff engineer interviews. The level guidance on this page provides specific tips for each level — SDE-2 candidates should focus on core architecture, while Staff engineers should discuss trade-offs, monitoring, and incremental rollouts.

Practice with a Mock Interview

Apply what you learned in a live system design mock interview with InterviewSkool's AI interviewer.

Start System Design Interview →