Email Unsubscribe System: Low-Level Design
An email unsubscribe system lets users opt out of specific email categories while maintaining compliance with CAN-SPAM, GDPR, and CASL regulations. The design must handle granular category preferences, one-click unsubscribe (RFC 8058), global unsubscribe, and the critical safety requirement that unsubscribes must be respected immediately and permanently — bouncing or re-subscribing a user without their consent is a compliance violation.
Core Data Model
CREATE TABLE EmailCategory (
category_id SERIAL PRIMARY KEY,
category_key VARCHAR(100) UNIQUE NOT NULL, -- 'marketing', 'product_updates', 'weekly_digest'
display_name VARCHAR(200) NOT NULL,
description TEXT,
is_transactional BOOLEAN NOT NULL DEFAULT FALSE, -- transactional emails cannot be unsubscribed
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE EmailPreference (
pref_id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
category_id INT NOT NULL REFERENCES EmailCategory(category_id),
subscribed BOOLEAN NOT NULL DEFAULT TRUE,
source VARCHAR(50) NOT NULL DEFAULT 'signup', -- signup, user_action, import
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_id, category_id) -- one preference per user per category
);
CREATE TABLE GlobalUnsubscribe (
user_id BIGINT PRIMARY KEY,
email VARCHAR(255) NOT NULL, -- denormalized for email-level lookups
reason VARCHAR(50), -- 'user_request', 'bounce_hard', 'complaint', 'admin'
unsubscribed_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE UnsubscribeAuditLog (
log_id BIGSERIAL PRIMARY KEY,
user_id BIGINT,
email VARCHAR(255) NOT NULL,
action VARCHAR(30) NOT NULL, -- 'unsubscribe', 'resubscribe', 'global_unsubscribe'
category_key VARCHAR(100), -- NULL for global
source VARCHAR(50) NOT NULL, -- 'one_click', 'preference_page', 'bounce', 'api'
ip_address INET,
user_agent VARCHAR(500),
logged_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX ON EmailPreference(user_id);
CREATE INDEX ON GlobalUnsubscribe(email);
Checking Send Permission Before Each Email
def can_send(user_id: int, category_key: str) -> bool:
"""
Check if a user can receive an email in the given category.
Must be called before every email send — compliance requirement.
"""
# 1. Global unsubscribe — hard stop for all non-transactional email
if db.fetchone("SELECT 1 FROM GlobalUnsubscribe WHERE user_id=%s", (user_id,)):
category = db.fetchone(
"SELECT is_transactional FROM EmailCategory WHERE category_key=%s", (category_key,)
)
if not category or not category['is_transactional']:
return False
# 2. Category-level preference
pref = db.fetchone("""
SELECT ep.subscribed, ec.is_transactional
FROM EmailPreference ep
JOIN EmailCategory ec USING (category_id)
WHERE ep.user_id=%s AND ec.category_key=%s
""", (user_id, category_key))
if pref:
# Transactional emails ignore preference — always send
if pref['is_transactional']:
return True
return pref['subscribed']
# 3. No explicit preference — check default for the category
category = db.fetchone(
"SELECT is_transactional FROM EmailCategory WHERE category_key=%s", (category_key,)
)
# Default: send if no preference on file (opt-out model)
# For GDPR double-opt-in products: change default to False (opt-in model)
return True if category else False
Unsubscribe Handlers
def unsubscribe_category(user_id: int, category_key: str,
source: str = 'user_action', ip: str = None):
"""Unsubscribe from a specific category."""
category = db.fetchone(
"SELECT category_id, is_transactional FROM EmailCategory WHERE category_key=%s",
(category_key,)
)
if not category:
raise ValueError(f"Unknown category: {category_key}")
if category['is_transactional']:
raise CannotUnsubscribeTransactionalError(
"Transactional emails (receipts, security alerts) cannot be unsubscribed"
)
db.execute("""
INSERT INTO EmailPreference (user_id, category_id, subscribed, source)
VALUES (%s, %s, FALSE, %s)
ON CONFLICT (user_id, category_id) DO UPDATE
SET subscribed=FALSE, source=EXCLUDED.source, updated_at=NOW()
""", (user_id, category['category_id'], source))
_log_action(user_id, None, 'unsubscribe', category_key, source, ip)
def global_unsubscribe(user_id: int, email: str,
reason: str = 'user_request', source: str = 'user_action',
ip: str = None):
"""Opt out of all non-transactional email."""
db.execute("""
INSERT INTO GlobalUnsubscribe (user_id, email, reason)
VALUES (%s, %s, %s)
ON CONFLICT (user_id) DO UPDATE
SET email=EXCLUDED.email, reason=EXCLUDED.reason, unsubscribed_at=NOW()
""", (user_id, email, reason))
_log_action(user_id, email, 'global_unsubscribe', None, source, ip)
def one_click_unsubscribe(token: str, ip: str = None):
"""
RFC 8058 / List-Unsubscribe: POST to unsubscribe endpoint with a signed token.
Token = HMAC-SHA256(user_id:category_key:email, secret_key), base64url encoded.
"""
import hmac, hashlib, base64
try:
payload, sig = token.rsplit('.', 1)
expected = base64.urlsafe_b64encode(
hmac.new(SECRET_KEY, payload.encode(), hashlib.sha256).digest()
).rstrip(b'=').decode()
if not hmac.compare_digest(sig, expected):
raise InvalidTokenError("Invalid unsubscribe token")
user_id, category_key, email = base64.urlsafe_b64decode(
payload + '=='
).decode().split(':')
unsubscribe_category(int(user_id), category_key, source='one_click', ip=ip)
except (ValueError, KeyError) as e:
raise InvalidTokenError("Malformed unsubscribe token") from e
def _log_action(user_id, email, action, category_key, source, ip):
user_email = email
if not user_email and user_id:
row = db.fetchone("SELECT email FROM User WHERE user_id=%s", (user_id,))
user_email = row['email'] if row else None
db.execute("""
INSERT INTO UnsubscribeAuditLog
(user_id, email, action, category_key, source, ip_address)
VALUES (%s,%s,%s,%s,%s,%s)
""", (user_id, user_email, action, category_key, source, ip))
Key Design Decisions
- Global unsubscribe takes precedence: can_send() checks GlobalUnsubscribe first — one row blocks all marketing email regardless of category preferences. This is a CAN-SPAM requirement: an opt-out of all email must be honored within 10 business days (immediate in practice).
- Transactional emails bypass unsubscribe: password resets, receipts, security alerts, and account notifications are is_transactional=TRUE and are sent regardless of global unsubscribe status. Never mark marketing emails as transactional to work around unsubscribes — this violates CAN-SPAM and GDPR and destroys sender reputation.
- HMAC-signed one-click tokens: one-click unsubscribe tokens must not contain guessable or enumerable values. The HMAC signature prevents forged unsubscribe requests. The token embeds user_id, category, and email so the handler doesn’t need a database lookup to identify the subscriber.
- Audit log for compliance: every preference change is logged with source, IP, and timestamp. CAN-SPAM requires that you can demonstrate an opt-out was honored. GDPR requires a record of consent changes. The audit log provides this evidence for regulatory inquiries.
Email unsubscribe and notification preference system design is discussed in LinkedIn system design interview questions.
Email unsubscribe and merchant email compliance design is covered in Shopify system design interview preparation.
Email unsubscribe and customer communication compliance design is discussed in Amazon system design interview guide.