GDPR Engineering Compliance

Implement GDPR technical requirements in software systems. Covers data subject rights automation, consent management, data minimization, right to erasure, data portability, privacy by design, and the patterns that make GDPR compliance a technical capability rather than a legal burden.

GDPR Engineering Compliance is the process of ensuring that an engineering system actively enforces and evidences compliance with the General Data Protection Regulation. It matters when user data is collected, processed, stored, or shared across systems—especially in regulated environments where data subject rights, data minimisation, and accountability must be implemented and auditable.

Data Subject Rights Implementation

Implementing the right to access, rectification, erasure, and portability requires structured data flows and clear interfaces. The core pattern is: a request arrives via a service endpoint, is validated, then triggers a sequence of operations across storage, processing, and metadata layers.

A GET /api/v1/data-subjects/{id} endpoint returns the full data set for a user, with X-Data-Subject-Id: 12345 and Accept: application/vnd.gdpr.v1+json. The response includes a X-Data-Subject-Consent-Records: 3 header and a consent_history array:

{
  "personal_data": {
    "name": "Amina Kassam",
    "email": "[email protected]",
    "phone": "+44 20 7946 0958"
  },
  "consent_history": [
    {
      "consent_id": "c123",
      "purpose": "marketing",
      "granted_at": "2023-04-15T10:30:00Z",
      "expires_at": "2024-04-15T10:30:00Z",
      "consent_source": "web_form",
      "consent_version": "1.2"
    },
    {
      "consent_id": "c456",
      "purpose": "analytics",
      "granted_at": "2023-06-20T14:15:00Z",
      "expires_at": null,
      "consent_source": "mobile_app",
      "consent_version": "1.1"
    }
  ],
  "data_processing_activities": [
    {
      "activity_id": "pa789",
      "description": "User profile update",
      "controller": "[email protected]",
      "retention_period": "36 months",
      "legal_basis": "consent",
      "data_categories": ["personal", "transactional", "behavioral"]
    }
  ]
}

The POST /api/v1/data-subjects/{id}/requests endpoint accepts a data_subject_request object with type (access, rectification, erasure, portability, restriction), details, and requester. A rectification request must include field_updates as a list of { field: "email", old_value: "[email protected]", new_value: "[email protected]" }.

The system must record a DataSubjectRequest event in a data_subject_requests table with fields: id, user_id, type, status (pending, in_progress, completed, failed), submitted_at, completed_at, details, and metadata. A failed status includes a error_code and error_message. On failure, the system sends a notification to system:gdpr:requests:failed via a Kafka topic.

Consent is not a single flag but a time-bound, purpose-specific, and versioned record. When a user grants consent, the system writes a consents record with user_id, purpose, granted_at, expires_at, consent_source, and version.

A PUT /api/v1/consents/{id} endpoint updates the consent, allowing a renewed_at timestamp and new_version. The system enforces expiration by a scheduled job (cron:consent-expiration-check) that runs every 12 hours and executes:

kubectl exec -it cron-consent-expiration-check-7d9b8f5c4b -- \
  sh -c '
    psql -d gdpr -c "
      WITH expired AS (
        SELECT c.id, c.user_id, c.purpose
        FROM consents c
        WHERE c.expires_at <= NOW() AND c.status = ''active''
      )
      UPDATE consents
      SET status = ''expired'', updated_at = NOW()
      FROM expired e
      WHERE consents.id = e.id;
    "
  '

When a consent is expired, the system triggers a ConsentExpiredEvent in a message queue, which may be consumed by a service that sends email notifications. The event includes user_id, consent_id, purpose, expired_at, and reason (expiry, withdrawal, revocation).

The GET /api/v1/consents endpoint supports pagination and filtering by user_id, purpose, status, from, and to. When a user requests access to their data, the system includes all consent records valid at the time of request.

Data Minimisation and Lifecycle

Data minimisation requires that each data processing activity defines which categories of data are collected, and that the system enforces this across ingestion and storage.

A data_processing_activity is defined with activity_id, description, controller, legal_basis, retention_period, and data_categories. The data_categories array must be one or more of: personal, contact, transactional, demographic, behavioral, financial, health, geolocation.

When a new user signs up via a POST /api/v1/users request, the system validates that data_categories in the request match the data_categories defined for the sign_up activity. If the request includes email, phone, preferences, and address, but the activity defines only personal, transactional, and demographic, the system logs a DataCategoryMismatchWarning in the data_processing_logs table.

The system enforces retention by a cron:retention-sweep job that runs daily and executes:

psql -d gdpr -c "
  WITH expired AS (
    SELECT e.id, e.user_id, e.activity_id, e.created_at
    FROM event_logs e
    JOIN data_processing_activities dpa ON e.activity_id = dpa.activity_id
    WHERE e.created_at < NOW() - dpa.retention_period
  )
  DELETE FROM event_logs
  WHERE id IN (SELECT id FROM expired)
  RETURNING id, user_id, activity_id, created_at;
"

The job logs the number of deleted records and any errors. A failed retention sweep includes a error_log field with the output of the psql command.

Anonymisation and Pseudonymisation

Pseudonymisation is enforced at ingestion. When a record is inserted into a user_profiles table, the system runs:

INSERT INTO user_profiles (id, user_id, first_name, last_name, email, address, phone, created_at)
VALUES (nextval('user_profiles_id_seq'), $1, $2, $3, $4, $5, $6, NOW())
ON CONFLICT (user_id) DO UPDATE
SET
  first_name = EXCLUDED.first_name,
  last_name = EXCLUDED.last_name,
  email = EXCLUDED.email,
  address = EXCLUDED.address,
  phone = EXCLUDED.phone,
  updated_at = NOW();

-- Pseudonymise
UPDATE user_profiles
SET
  email = md5(email || 'salt-for-gdpr-pseudonymisation'),
  phone = concat('p-', substr(md5(phone || 'salt-for-gdpr-pseudonymisation'), 1, 8))
WHERE pseudonymised_at IS NULL;

The system tracks pseudonymisation in a pseudonymisation_logs table with id, user_id, source_field, pseudonym, salt, timestamp, and source_value. A GET /api/v1/pseudonymisation/{id} endpoint returns the mapping.

For anonymisation, a POST /api/v1/anonymisation/jobs endpoint creates a job that processes data over a time range. The job includes source_table, target_table, anonymisation_rules, and schedule. A rule is a field, method, and parameters.

An example job:

{
  "job_id": "an123",
  "source_table": "user_profiles",
  "target_table": "user_profiles_anonymised",
  "anonymisation_rules": [
    {
      "field": "email",
      "method": "k-anonymity",
      "parameters": {
        "k": 5,
        "group_by": ["city", "age_group"]
      }
    },
    {
      "field": "first_name",
      "method": "hash",
      "parameters": {
        "algorithm": "sha256",
        "salt": "anonymise-2023"
      }
    }
  ],
  "schedule": "0 2 * * *",
  "status": "scheduled"
}

The job runs via a worker:anonymisation process that consumes anonymisation_jobs from a queue. On failure, the job status is updated to failed, and an error is logged in anonymisation_errors.

Data Processing Records and Accountability

Every data processing activity must be documented in a data_processing_records table with record_id, activity_id, controller, processing_purpose, legal_basis, data_categories, retention_period, data_subjects, data_transfers, and created_at.

A GET /api/v1/data-processing-records endpoint returns a list of records with filters by controller, from, to, purpose, and data_category. The endpoint includes X-Total-Count and X-Page-Size headers.

When a new processing activity is registered, the system generates a DataProcessingRecordCreatedEvent in a message queue. The event includes record_id, activity_id, controller, created_at, and metadata.

The system maintains a data_subject_rights_log table to track all actions taken in response to data subject requests. Each entry includes request_id, user_id, action_type (access, rectification, erasure, portability, restriction), details, executed_at, executor, and status.

Cross-Cutting Mechanisms

Audit Trail Integration

Every data operation that affects GDPR compliance must emit an audit event. The event format is:

{
  "event_type": "data_subject_request_submitted",
  "timestamp": "2023-09-20T11:23:45Z",
  "user_id": 12345,
  "request_id": "req-9876",
  "payload": {
    "type": "access",
    "details": {
      "requested_fields": ["email", "preferences", "purchase_history"],
      "include_consent_history": true
    },
    "requester": "user"
  },
  "metadata": {
    "source": "api",
    "ip_address": "192.168.1.10",
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
  }
}

The system writes these events to a audit_logs table with id, event_type, timestamp, user_id, request_id, payload, and metadata. The audit_logs table is indexed by event_type, timestamp, and user_id.

When a new version of a consent form is released, the system must allow both old and new versions to coexist. The consents table includes a version field (e.g., 1.0, 1.1, 2.0). A GET /api/v1/consents/{id} endpoint returns current_version and available_versions.

The system maintains a consent_forms table with form_id, version, title, description, fields, created_at, updated_at, and active.

When a user opts in via a form, the system records which form_id and form_version were used. This enables tracking of consent adoption over time.

Data Subject Request Orchestration

A DataSubjectRequest is not a single action but a sequence of steps. The data_subject_requests table includes a steps array of step_id, type, status, started_at, completed_at, error, and payload.

A rectification request may include the following steps:

  1. validate_request — checks details and requester
  2. fetch_user_data — retrieves data from multiple services
  3. apply_changes — updates database records
  4. notify_user — sends email confirmation

Each step may fail independently. The system logs step failures with error and error_details fields. On failure, the request status is updated to failed, and an alert is sent to system:gdpr:requests:failed.

Sharp Edges

Failure Modes

These failures are logged in system:gdpr:errors and monitored via a compliance dashboard.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.