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.
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 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.
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.
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.
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.
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:
validate_request — checks details and requesterfetch_user_data — retrieves data from multiple servicesapply_changes — updates database recordsnotify_user — sends email confirmationEach 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.
expires_at and also consider withdrawn_at if a user revokes consent. A consent is active only if granted_at ≤ now ≤ (expires_at OR withdrawn_at).data_categories must be from a predefined set. Adding an unknown category (e.g., “socio-economic”) triggers a DataCategoryValidationError.NOW() consistently across steps.completed_rows, failed_rows, and error_log.metadata field is critical for understanding context.request_id should not create duplicate actions. The system uses request_id as a unique key in data_subject_requests.GET /api/v1/data-subjects/{id} request returns 404 if data_subjects.id does not exist, but a 400 if data_subjects.user_id is null or missing.POST /api/v1/data-subjects/{id}/requests request returns 422 Unprocessable Entity if type is not one of access, rectification, erasure, portability, restriction.PUT /api/v1/consents/{id} request returns 409 Conflict if version is outdated (e.g., client sends version: 1.1, but server has 1.2).POST /api/v1/anonymisation/jobs request returns 400 Bad Request with errors array if any rule is invalid.GET /api/v1/data-processing-records endpoint returns 500 Internal Server Error if data_processing_records table is missing or has inconsistent schema.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.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy