Consent Management System Low-Level Design

What is Consent Management?

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);

Consent Check (Hot Path)

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)

Consent Collection Flow

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()
        })

Policy Version Re-Consent

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

{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”What data does a GDPR-compliant consent record need to capture?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”A GDPR-compliant consent record must capture: who gave consent (user_id), for what purpose (purpose_id), what decision was made (granted=true/false), when (created_at with timezone), the privacy policy version in effect (policy_version), how consent was collected (source: signup_form, cookie_banner), and proof of context (ip_address, user_agent). All records must be immutable — never update; only append new records. You must be able to demonstrate for any user, at any point in time, what their consent status was.”}},{“@type”:”Question”,”name”:”How do you enforce consent in real time without slowing down every API call?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Cache consent status per user in Redis: a hash of {purpose_id: granted/denied} with TTL=5min. On every check (before sending marketing email, before tracking analytics event), call has_consent(user_id, purpose_id) which reads from Redis first. On consent change: immediately delete the Redis cache key. The next check will reload from DB and re-cache. This means consent changes propagate within 5 minutes (cache TTL) or immediately (cache invalidation on change). Never skip consent checks for performance — the latency cost of a Redis read is ~1ms.”}},{“@type”:”Question”,”name”:”How do you handle consent when users withdraw it?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Record a new ConsentRecord with granted=false (withdrawal is an event, not a deletion of the original). Invalidate the Redis cache. Publish a consent-withdrawn event to Kafka so downstream systems can react: marketing service removes the user from all active campaigns; analytics pipeline stops tracking new events; personalization engine removes the user’s profile data. Withdrawal must be as easy as granting consent — GDPR Article 7(3) requires this. Honor withdrawal for all future processing; historical data logged before withdrawal is not retroactively deleted.”}},{“@type”:”Question”,”name”:”What happens when you update your privacy policy?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Bump the policy_version identifier. Mark purposes with material changes as requiring re-consent. On each user’s next login, show a consent dialog for the updated purposes. Users who decline or ignore: disable the affected features for them. Set a deadline (30 days is common) after which users who haven’t responded are treated as having declined. Store the policy_version on each consent record so you can always show what policy the user was consenting to at the time they gave consent.”}},{“@type”:”Question”,”name”:”How do you export all consent records for a GDPR audit?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Query ConsentRecord WHERE user_id=:uid ORDER BY created_at ASC — this gives the complete chronological history of every consent event for the user. Export includes: record_id, purpose_id, purpose_name, granted, policy_version, source, ip_address, created_at. This is also the response to a GDPR Subject Access Request for consent data. For a compliance audit covering all users: export to CSV/JSON via the async export service, partitioned by month for large datasets.”}}]}

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