Consent Management System Low-Level Design

Consent management records and enforces users’ privacy preferences: which data collection and processing activities they have opted into or out of. Required by GDPR (EU), CCPA (California), and similar laws globally. A Consent Management Platform (CMP) collects consent at account creation and on cookie banners, stores consent records with timestamps and legal basis, and enforces those preferences throughout the system. OneTrust, Didomi, and Cookiebot are commercial CMPs; building your own is required when integrating consent into core product logic.

Requirements

  • Record per-user consent for each processing purpose (analytics, marketing, personalization)
  • Immutable consent history: every change is recorded, never overwritten
  • Enforce consent: marketing system checks consent before sending; analytics checks before tracking
  • Consent version: when your privacy policy changes, collect fresh consent
  • Withdrawal: users can withdraw consent at any time; enforcement is near-immediate
  • Export all consent records for GDPR audit

Data Model

ConsentPurpose(
    purpose_id  VARCHAR PRIMARY KEY,  -- 'analytics', 'marketing_email', 'personalization'
    name        VARCHAR,
    description TEXT,
    legal_basis ENUM(CONSENT, LEGITIMATE_INTEREST, CONTRACT, LEGAL_OBLIGATION),
    required    BOOL DEFAULT false    -- required purposes can't be withdrawn
)

ConsentRecord(
    record_id   UUID PRIMARY KEY,
    user_id     UUID NOT NULL,
    purpose_id  VARCHAR NOT NULL,
    granted     BOOL NOT NULL,        -- true = opt-in, false = opt-out/withdrawal
    policy_version VARCHAR,           -- privacy policy version at time of consent
    source      VARCHAR,              -- 'signup_form', 'cookie_banner', 'settings_page'
    ip_address  VARCHAR,
    user_agent  VARCHAR,
    created_at  TIMESTAMPTZ NOT NULL  -- immutable, never updated

    -- Latest record per (user_id, purpose_id) = current consent
)

CREATE INDEX idx_consent_lookup ON ConsentRecord(user_id, purpose_id, created_at DESC);
def has_consent(user_id, purpose_id):
    # Cache: Redis hash of {purpose_id: granted} per user, TTL=5min
    key = f'consent:{user_id}'
    cached = redis.hget(key, purpose_id)
    if cached is not None:
        return cached == '1'

    # DB: get the most recent consent record for this purpose
    record = db.query('''
        SELECT granted FROM ConsentRecord
        WHERE user_id=:uid AND purpose_id=:pid
        ORDER BY created_at DESC LIMIT 1
    ''', uid=user_id, pid=purpose_id).first()

    result = record.granted if record else False  # no record = no consent

    # Cache purpose consent for 5 minutes
    redis.hset(key, purpose_id, '1' if result else '0')
    redis.expire(key, 300)
    return result

# Usage throughout the system:
if has_consent(user_id, 'marketing_email'):
    send_marketing_email(user_id, campaign)

if has_consent(user_id, 'analytics'):
    track_event(user_id, 'page_view', properties)
def record_consent(user_id, purposes, granted, source, ip, user_agent):
    # Insert immutable consent records for each purpose
    records = []
    for purpose_id in purposes:
        records.append(ConsentRecord(
            user_id=user_id,
            purpose_id=purpose_id,
            granted=granted,
            policy_version=get_current_policy_version(),
            source=source,
            ip_address=ip,
            user_agent=user_agent,
            created_at=now()
        ))
    db.bulk_insert(records)

    # Invalidate Redis cache so next check reads fresh state
    redis.delete(f'consent:{user_id}')

    # Emit event for downstream systems (analytics pipeline, marketing suppression)
    for purpose_id in purposes:
        kafka.produce('consent-changes', {
            'user_id': user_id,
            'purpose_id': purpose_id,
            'granted': granted,
            'timestamp': now().isoformat()
        })

When the privacy policy changes materially: bump the policy version. Mark all consent records for affected purposes as requiring re-consent. On next login, show the updated consent dialog. Users who don’t consent within 30 days: disable affected features.

def needs_reconsent(user_id):
    current_version = get_current_policy_version()
    user_version = db.query('''
        SELECT MAX(policy_version) FROM ConsentRecord
        WHERE user_id=:uid AND granted=true
    ''', uid=user_id).scalar()
    return user_version != current_version

Key Design Decisions

  • Append-only consent records — immutability is a legal requirement; every consent and withdrawal is recorded with timestamp and source
  • Redis cache for consent checks — every marketing send and analytics event checks consent; DB query on every event would be too slow
  • Kafka for consent change events — downstream systems (marketing suppression lists, analytics filters) update asynchronously
  • No record = no consent — conservative default; require explicit opt-in for non-required purposes
  • Source tracking (signup_form, cookie_banner) — required for GDPR audit to prove valid consent collection

Consent management and privacy system design is discussed in Meta system design interview questions.

GDPR consent management and data privacy design is covered in Google system design interview preparation.

Privacy consent and data protection system design is discussed in Apple system design interview guide.

See also: Stripe Interview Guide 2026: Process, Bug Bash Round, and Payment Systems

Scroll to Top