Gdpr Engineering Requirements

Production-grade guide to gdpr engineering requirements covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.

GDPR Engineering Requirements define the technical implementation expectations for systems handling personal data of individuals in the European Union. These requirements are triggered when a system must satisfy legal obligations under the General Data Protection Regulation, particularly around data subject rights, data processing transparency, and accountability. They are most critical during system design, compliance audits, and when responding to data subject requests or regulatory inquiries.

Data Subject Rights Implementation

Right to Access (Article 15)

To implement the right to access, systems must expose a query endpoint that returns all personal data a controller holds on a specific individual. The endpoint accepts a subject_id and returns a JSON object with the following structure:

{
  "subject_id": "usr-7f3a2b1c",
  "personal_data": [
    {
      "field": "email",
      "value": "[email protected]",
      "source": "user_registration_form",
      "updated_at": "2023-06-14T10:23:45Z"
    },
    {
      "field": "name",
      "value": "Alice Johnson",
      "source": "customer_profile",
      "updated_at": "2023-04-05T14:12:30Z"
    },
    {
      "field": "address",
      "value": {
        "street": "123 Main St",
        "city": "Berlin",
        "postal_code": "10115",
        "country": "DE"
      },
      "source": "order_history",
      "updated_at": "2023-03-20T09:45:11Z"
    }
  ],
  "data_processing_activities": [
    {
      "purpose": "order fulfillment",
      "lawful_basis": "contract",
      "retention_period": "5 years",
      "third_parties": [
        "shipping_co_1",
        "payment_gateway_3"
      ],
      "consent": {
        "given_at": "2023-02-01T11:30:00Z",
        "consent_id": "consent-8a1c4b2d"
      }
    }
  ],
  "last_updated": "2023-06-14T10:23:45Z"
}

The endpoint must respond within 30 seconds for 95% of requests and support pagination via ?page=2&per_page=100. A 404 Not Found is returned if the subject_id is unknown. If the subject has no data, the personal_data array is empty, and data_processing_activities may be omitted.

Right to Rectification (Article 16)

When a subject requests correction of their data, the system must support a PATCH /api/v1/data-subjects/{subject_id} endpoint. The request body is a JSON object with a changes array:

{
  "changes": [
    {
      "field": "email",
      "new_value": "[email protected]",
      "reason": "Changed job",
      "timestamp": "2023-06-15T08:00:00Z"
    },
    {
      "field": "address",
      "new_value": {
        "street": "456 Oak Avenue",
        "city": "Hamburg",
        "postal_code": "20095",
        "country": "DE"
      },
      "reason": "Moved to new apartment",
      "timestamp": "2023-06-15T08:00:00Z"
    }
  ]
}

The server must apply changes atomically: if any field fails to update, all changes are rolled back. A 200 OK is returned with a X-Operation-ID header set to a UUID. The response body includes a status field with values pending, applied, or failed, and a details object with applied_fields, failed_fields, and error_messages.

Silent failure occurs when the field path is malformed: address.street is valid, but address.streets is not. The server does not validate nested objects beyond the first level unless explicitly configured. The reason field is optional but must be logged to the audit trail.

Right to Erasure (Article 17)

To support the right to erase, a system must provide a DELETE /api/v1/data-subjects/{subject_id} endpoint with optional ?permanent=true and ?reason=marketing_opt_out.

A permanent=true request deletes all traces of the subject: data in databases, files, logs, and backups. The endpoint returns 202 Accepted with a Location header pointing to a status check endpoint: GET /api/v1/operations/{operation_id}.

{
  "operation_id": "op-3a2b1c4d",
  "status": "in_progress",
  "progress": 0.45,
  "started_at": "2023-06-15T08:10:00Z",
  "estimated_completion": "2023-06-15T08:45:00Z"
}

The operation is tracked in a data_operations table with columns: id, subject_id, type, status, started_at, completed_at, progress, details. The details column is a JSON field containing deleted_tables, deleted_files, total_records, and errors.

Failure mode: if the permanent flag is true but a required dependency is down (e.g., S3 is unreachable), the operation fails with status: failed, and details.errors includes s3_connection_timeout, error_code: 504.

Consent is recorded in a consents table with the following schema:

ColumnTypeConstraints
idUUIDprimary key
subject_idUUIDforeign key
purposetextnot null
granted_attimestampnot null
expires_attimestampnullable
sourcetextdefault 'web'
metadatajsonbnullable
revoked_attimestampnullable

A consent is granted via POST /api/v1/consents with a JSON body:

{
  "subject_id": "usr-7f3a2b1c",
  "purpose": "marketing_emails",
  "expires_at": "2025-06-14T23:59:59Z",
  "source": "mobile_app",
  "metadata": {
    "campaign_id": "summer2023",
    "channel": "push_notification"
  }
}

The server returns 201 Created with Location header pointing to the new consent’s URI. The metadata field is stored as-is and is queryable via JSON operators.

The expires_at field is critical: if expires_at is not set, the consent is valid indefinitely. A GET /api/v1/consents?subject_id=usr-7f3a2b1c&purpose=marketing_emails returns all consents for that purpose, sorted by granted_at.

Revocation is implemented by setting revoked_at to the current time and returning 200 OK with Content-Type: application/json:

{
  "consent_id": "consent-8a1c4b2d",
  "revoked_at": "2023-06-15T08:30:00Z",
  "status": "revoked",
  "inherited_from": "consent-1a2b3c4d"
}

The inherited_from field links to the consent that was the basis for this one, used when a group consent is revoked but individual consent persists.

A background job runs every 15 minutes to check for expired consents. It queries:

SELECT id, subject_id
FROM consents
WHERE expires_at <= NOW() AND revoked_at IS NULL;

For each expired consent, it inserts a record into consent_expirations with consent_id, triggered_at, and reason: 'expiry'.

Data Processing Transparency

Data Processing Register (Article 30)

The register is a collection of data processing activities, each described by a record in the data_processing_activities table with:

ColumnTypeConstraints
idUUIDprimary key
controller_nametextnot null
purposetextnot null
legal_basistextnot null
data_retention_periodintervaldefault '3 years'
third_party_idstext[]nullable
data_categorytext[]not null
processing_locationtextdefault 'EU'

The register is exposed via GET /api/v1/data-processing-register with optional query parameters:

The response is a list of objects:

[
  {
    "id": "dpa-12345",
    "controller_name": "Acme Corp",
    "purpose": "customer support",
    "legal_basis": "contract",
    "data_retention_period": "2 years",
    "third_party_ids": ["support_ticketing_system", "ai_chatbot"],
    "data_category": ["personal", "contact", "transaction"],
    "processing_location": "EU"
  }
]

The data_category field allows filtering and is used for reporting and audit purposes. The third_party_ids list is not indexed by default, so queries on it require jsonb or array indexing.

Accountability and Audit Trail

Audit Trail Integration

All data operations must be logged to a central audit trail. Each operation emits a record to a audit_logs table:

ColumnTypeConstraints
idUUIDprimary key
operation_typetextnot null
subject_idUUIDnullable
detailsjsonbnot null
occurred_attimestampnot null
source_systemtextdefault 'core-api'

For example, a data access request generates:

{
  "operation_type": "data_access",
  "subject_id": "usr-7f3a2b1c",
  "details": {
    "request_id": "req-9a8b7c6d",
    "requester_ip": "192.168.1.10",
    "requester_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
  },
  "occurred_at": "2023-06-14T10:23:45Z",
  "source_system": "data-access-service"
}

The details field is used for both structured and arbitrary data, and can be queried using JSON operators.

A critical failure mode: if subject_id is NULL but the operation is tied to a specific user, the audit log is less useful for tracing individual data subject activities. Systems often forget to populate subject_id for batch operations.

Data Protection Impact Assessments (DPIA) Tracking

DPIA results are stored in a dpias table:

ColumnTypeConstraints
idUUIDprimary key
nametextnot null
statustextnot null
risk_leveltextdefault 'medium'
assessed_attimestampnot null
findingsjsonbnullable
action_itemsjsonbnot null

The action_items field contains a list of tasks, each with id, description, assigned_to, due_date, status. The findings field is free-form but includes risk_factors, recommendations, and evidence.

A GET /api/v1/dpias?status=completed&risk_level=high returns all high-risk completed DPIAs.

The system must ensure that any new data processing activity with a risk level of high must have a DPIA. This is enforced by a validation step in the data processing register creation workflow.

Data Portability

Export of Personal Data (Article 20)

To support data portability, systems must provide a GET /api/v1/data-subjects/{subject_id}/export endpoint that returns a ZIP file containing:

{
  "export_id": "exp-1a2b3c4d",
  "exported_at": "2023-06-15T09:00:00Z",
  "format": "json",
  "version": "1.0",
  "source_system": "acme-core",
  "data_subject_id": "usr-7f3a2b1c"
}

The ZIP file is served with Content-Type: application/zip and Content-Disposition: attachment; filename="data_export_7f3a2b1c.zip".

The export is asynchronous: a 202 Accepted is returned with Location pointing to a status check endpoint. The ZIP file is stored in object storage (e.g., AWS S3, GCP Cloud Storage) and is available for 30 days.

Silent failure: if the personal_data.json file is not included in the ZIP, the export is considered invalid. The system must validate the ZIP structure before marking the export as complete.

Format and Interoperability

The export must be in a standard format: JSON, XML, or CSV. The default is JSON. When exporting to CSV, each table becomes a separate file. The personal_data table becomes personal_data.csv, and the consents table becomes consents.csv.

The data_processing_activities table is exported as data_processing_activities.csv with columns: id, controller_name, purpose, legal_basis, data_retention_period, third_party_ids, data_category, processing_location.

The data_category and third_party_ids fields are comma-separated in the CSV. The data_category field may contain multiple values: "personal, contact, transaction".

Data Retention and Deletion

Retention Policies and Automation

Retention policies are configured in a retention_policies table:

ColumnTypeConstraints
idUUIDprimary key
data_categorytextnot null
retention_periodintervalnot null
retention_actiontextdefault 'archive_and_delete'
last_runtimestampnullable

The system runs a daily job that checks retention_policies and processes data older than retention_period. The job queries:

SELECT id, data_category, retention_period, retention_action
FROM retention_policies
WHERE last_run <= NOW() - INTERVAL '1 day';

For each policy, it runs:

WITH expired AS (
  SELECT id
  FROM personal_data
  WHERE created_at <= NOW() - retention_policies.retention_period
  AND category = retention_policies.data_category
)
UPDATE retention_policies
SET last_run = NOW()
WHERE id IN (SELECT id FROM expired);

The retention_action field determines the next steps:

Failure: if archive_and_delete is used but the archive system is down, the job logs an error and continues, but does not update last_run. The next day, the same data is processed again.

Data Subject Requests Lifecycle

Request Handling and State Management

All data subject requests are tracked in a data_subject_requests table with:

ColumnTypeConstraints
idUUIDprimary key
subject_idUUIDnot null
typetextnot null
statustextdefault 'received'
submitted_attimestampnot null
completed_attimestampnullable
metadatajsonbnullable

The type field includes: access, rectification, erasure, consent, portability, restriction.

The state machine progresses as:

  1. received → processing → completed or failed
  2. failed → retried → completed

A PATCH /api/v1/data-subject-requests/{id} endpoint updates the status and metadata:

{
  "status": "processing",
  "metadata": {
    "assigned_to": "support_team",
    "priority": "high",
    "estimated_time": "2 days"
  }
}

The system must send email notifications on status changes. The metadata field is used to store arbitrary request-specific data, such as reason for erasure or new_email for rectification.

The request lifecycle is the backbone of compliance: every audit, every subject inquiry, every regulatory response traces back to this state machine.

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.