What is a Newsletter System?
A newsletter system manages subscriber lists, drafts and schedules email campaigns, personalizes content, and tracks delivery metrics (open rate, click rate, unsubscribes). Substack, Mailchimp, and Beehiiv are dedicated newsletter platforms. At scale (tens of millions of subscribers), the hard problems are email deliverability (avoiding spam folders), high-throughput sending (millions of emails per hour), and list hygiene (bounces, unsubscribes).
Requirements
- Manage subscriber lists; support segments (e.g., “paid subscribers in US”)
- Create and schedule email campaigns (HTML templates with personalization variables)
- Send 5 million emails per hour during campaign launches
- Track opens (pixel tracking), link clicks (redirect tracking), bounces, unsubscribes
- Honor unsubscribes immediately (CAN-SPAM compliance)
- Deliverability: >95% inbox placement rate
Data Model
Subscriber(subscriber_id UUID, email VARCHAR UNIQUE, name VARCHAR,
status ENUM(ACTIVE, UNSUBSCRIBED, BOUNCED, COMPLAINED),
subscribed_at, unsubscribed_at, source VARCHAR)
SubscriberAttribute(subscriber_id UUID, key VARCHAR, value VARCHAR)
-- stores arbitrary segmentation data: country, plan_type, signup_cohort
Campaign(campaign_id UUID, name VARCHAR,
status ENUM(DRAFT, SCHEDULED, SENDING, SENT, CANCELLED),
subject VARCHAR, from_name VARCHAR, from_email VARCHAR,
html_content TEXT, text_content TEXT,
segment_query JSONB, -- e.g., {"country": "US", "plan": "paid"}
scheduled_at TIMESTAMP,
sent_at TIMESTAMP, total_recipients INT)
CampaignSend(send_id UUID, campaign_id UUID, subscriber_id UUID,
status ENUM(QUEUED, SENT, DELIVERED, OPENED, CLICKED,
BOUNCED, UNSUBSCRIBED, COMPLAINED),
sent_at, opened_at, clicked_at, message_id VARCHAR)
-- message_id from ESP (SendGrid/SES message ID for bounce matching)
Sending Pipeline
1. Campaign is scheduled → cron triggers at scheduled_at 2. Segment query runs: SELECT subscriber_id FROM Subscribers JOIN SubscriberAttributes ... WHERE status='ACTIVE' AND ... 3. Batch inserts CampaignSend rows (QUEUED) in chunks of 10K 4. Queue workers pick up batches, render personalized HTML: html = template.render(name=subscriber.name, unsubscribe_url=...) 5. Call ESP API (SendGrid/AWS SES) to send: esp.send(to=email, subject=subject, html=html, message_id=uuid) 6. Update CampaignSend status to SENT, store message_id
Email Service Provider (ESP) Integration
Never operate your own mail servers for bulk sending — deliverability is hard. Use SendGrid, AWS SES, or Postmark. They maintain IP reputation, handle DKIM/SPF signing, and provide bounce/complaint webhooks.
# Sending via SendGrid API
sg = SendGridAPIClient(api_key=SENDGRID_KEY)
message = Mail(
from_email=('newsletters@example.com', 'Example Newsletter'),
to_emails=recipient_email,
subject=subject,
html_content=personalized_html
)
message.custom_arg = {'send_id': send_id} # for bounce/open webhook matching
sg.send(message)
Tracking and Webhooks
Open tracking: embed a 1×1 pixel in the email HTML: <img src=”https://track.example.com/open/{send_id}” />. When the email client loads the image, your server records the open and updates CampaignSend.opened_at. Note: many email clients block tracking pixels by default.
Click tracking: rewrite all links in the email to go through your redirect service: https://track.example.com/click/{send_id}/{link_hash} → original URL. Records the click, updates CampaignSend.clicked_at, then redirects.
Bounce/complaint handling: ESP sends webhooks for bounces (delivery failed) and complaints (user marked as spam). Consumer updates Subscriber.status: hard bounce → BOUNCED (never send again); spam complaint → COMPLAINED (immediate removal).
Deliverability Best Practices
- SPF, DKIM, DMARC records properly configured for your sending domain
- Dedicated IPs with warm-up schedule for new domains (start with 1K/day, ramp over 4-6 weeks)
- List hygiene: remove bounced and complained addresses immediately; scrub inactive subscribers monthly
- Double opt-in for new subscribers: require email confirmation before adding to list
- Unsubscribe link in every email (CAN-SPAM requirement); one-click unsubscribe per RFC 8058
Key Design Decisions
- ESP for bulk sending — never run your own mail servers; reputation management is too complex
- CampaignSend table per-recipient — enables per-subscriber open/click tracking and bounce handling
- Message ID from ESP stored locally — required to match bounce/complaint webhooks back to CampaignSend rows
- Async batch sending with queue — decouples campaign trigger from actual sending; allows retry on failure
- Segment query at send time, not signup — subscriber attributes change; evaluate segment fresh when sending
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you send 5 million emails per hour?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Use a dedicated Email Service Provider (ESP) like SendGrid, AWS SES, or Postmark rather than operating your own mail servers. ESPs maintain IP warm-up schedules and handle reputation management. On your side: batch CampaignSend rows into chunks of 10K, process with a pool of workers sending API requests in parallel. AWS SES supports up to 14M emails/day on dedicated IPs. Your bottleneck will be ESP API rate limits — negotiate higher throughput limits for bulk sending.”}},{“@type”:”Question”,”name”:”How does open tracking work in email newsletters?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Embed a 1×1 transparent pixel in each email’s HTML: <img src="https://track.example.com/open/{send_id}" width="1" height="1">. When the recipient’s email client loads images, it fetches this URL and your server logs the open event, updating CampaignSend.opened_at. Limitation: many clients (Apple Mail with Mail Privacy Protection, Outlook) pre-fetch images or block them, inflating or suppressing open rates. Treat open rate as directional, not exact.”}},{“@type”:”Question”,”name”:”How do you handle email bounces and spam complaints?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Configure your ESP to send webhook events for bounces and complaints to your endpoint. On a hard bounce (550 user unknown): immediately set Subscriber.status=BOUNCED — never send to this address again. On a soft bounce (temporary delivery failure): retry up to 3 times, then mark BOUNCED. On a spam complaint (user clicked "Report Spam"): immediately set status=COMPLAINED and never send again. High complaint rates (>0.1%) trigger ESP account suspension.”}},{“@type”:”Question”,”name”:”How do you segment subscribers for targeted campaigns?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Store arbitrary subscriber attributes in a SubscriberAttribute table (key-value pairs). At campaign creation, define a segment query: {"country": "US", "plan": "paid", "signup_cohort": "2025"}. At send time, evaluate the segment query against current subscriber attributes. Never pre-compute segments — attributes change (plan upgrades, country relocation) and you want the current state at send time, not at segment definition time.”}},{“@type”:”Question”,”name”:”What deliverability setup is required for bulk email?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Configure SPF (authorized senders for your domain), DKIM (cryptographic signature on every email), and DMARC (policy for failed SPF/DKIM checks). Use dedicated sending IPs and warm them up gradually: start at 1K emails/day, double every 3-4 days over 4-6 weeks before sending full volume. Maintain list hygiene: remove bounced addresses immediately, scrub inactive subscribers monthly. Implement double opt-in to ensure list quality.”}}]}
Email campaign and newsletter system design is discussed in Shopify system design interview questions.
Newsletter and email delivery system design is covered in LinkedIn system design interview preparation.
High-volume email sending infrastructure is discussed in Amazon system design interview guide.