In short

GDPR Article 25 makes privacy a design requirement: build measures such as pseudonymization and minimisation in when you choose the means, and by default process only the data each purpose needs, for the time it needs, visible only to the people who need it. Article 32 asks for security appropriate to the risk, including encryption, resilience, timely restore and regular testing. Article 35 requires a DPIA before high-risk processing starts. In code that means a personal-data inventory checked in CI, private defaults, a DPIA screen at design review and subject requests that reach every store.

  • Article 25(2) names four things defaults must limit: the amount of personal data collected, the extent of processing, the storage period and accessibility. Data must not be made accessible to an indefinite number of people without the person's intervention.
  • Article 32(1) lists pseudonymization and encryption, ongoing confidentiality, integrity, availability and resilience, timely restore after an incident, and regular testing of the measures.
  • A DPIA is mandatory for the Article 35(3) cases and, under the EDPB-endorsed WP248 guidelines, in most cases when processing meets two of nine criteria such as sensitive data, large scale or scoring.
  • Answer subject requests within one month of receipt, extendable by two further months for complex or numerous requests (Art. 12(3)), and tell recipients about erasure (Art. 19).
  • Pseudonymized data stays personal data for whoever holds the key. The CJEU (C-413/23 P, September 2025) held it may not be personal for a recipient that cannot re-identify anyone.
  • The Digital Omnibus proposal (November 2025) would change breach notification and DPIA lists but not Articles 25 and 32; on 10 October 2026 it was still before Parliament and Council.

Verify it

  1. Add a column in a migration without an inventory entry and confirm the CI check fails.
  2. Create a new account and share link in a test environment and confirm optional fields are off, the profile is private and the link expires.
  3. Run an erasure request for a seeded test subject, then search every store in the inventory, including exports and indexes, for the subject's identifiers.
  4. Restore last night's backup into a test environment and confirm erased subjects are removed before the store serves traffic.
  5. Pick one feature that processes special category data and confirm its fields link to a completed DPIA.
Applies to
  • Regulation (EU) 2016/679 (GDPR) Applies since 25 May 2018; text unchanged by the Digital Omnibus proposal as of 10 October 2026
  • EDPB Guidelines 4/2019 on Article 25 Version 2.0, adopted 20 October 2020
  • WP248 rev.01 DPIA guidelines Revised 4 October 2017; endorsed by the EDPB on 25 May 2018

Controllers and processors subject to the GDPR, and the engineers who build their systems. Legal bases, roles and national rules need a privacy or legal reviewer; this guide covers the engineering evidence.

Part of
Workflow: Build an AppSec program
Learning goals and prerequisitesAbout 32 minutes: 8 to read and 24 for the exercise

What you will learn

  • Translate Articles 25 and 32 into defaults, inventory checks and testable security measures.
  • Screen a feature for a DPIA with the Article 35(3) cases and the nine WP248 criteria.
  • Build access and erasure requests that reach every store, notify recipients and survive a backup restore.

Before you start

Backend, data and platform engineers building products that process personal data of people in the EU, and the privacy reviewers who work with them

  • Know which databases, queues, exports and backups your product writes personal data to.
  • Read Python and run a script in CI.

Refresh first: GDPR, CCPA, HIPAA or the FTC rule: scope privacy and health-data obligations before choosing controls

Go to the guided practice

What do Articles 25, 32 and 35 ask of engineers?

Three articles carry most of the engineering work. They state outcomes, weighed against “the state of the art, the cost of implementation and the nature, scope, context and purposes of processing” and the risks to people, so the evidence you keep has to show the reasoning as well as the result. The EDPB's Guidelines 4/2019 on Article 25 stress that measures must be effective, not merely present.

What do Articles 25, 32 and 35 ask of engineers?
ArticleWhat it requiresWhat engineering builds and keeps
25(1) by designAppropriate technical and organizational measures, such as pseudonymization, designed to implement the principles, such as data minimisation, both when the means are chosen and during processingPrivacy requirements in design review, a personal-data inventory, pseudonymized identifiers in analytics, deletion built into each store
25(2) by defaultOnly the data necessary for each purpose: amount collected, extent of processing, storage period and accessibilityOptional fields off, sharing off, retention jobs per field, private-by-default visibility, tests that assert the defaults
32(1)Security appropriate to the risk, including pseudonymization and encryption, confidentiality, integrity, availability and resilience, timely restore, and regular testingEncryption configuration, access reviews, restore drills with timings, penetration and DAST results
32(4)People with access process personal data only on the controller's instructionsRole-based access with logged, reviewed production access
35A DPIA before high-risk processing: description, necessity and proportionality, risks and the measures to address themA screen at design review, the DPIA record, and a link from each high-risk field to its DPIA

How do you stop new personal data slipping in unreviewed?

Article 25 is hard to show if nobody knows where personal data lives. Keep an inventory in the repository that describes every column, topic and export with its category, purposes, legal basis and retention, and fail the build when the database and the inventory disagree. The same file feeds your Article 30 record of processing activities, the retention jobs and the stores a subject request must reach.

Below, the inventory has an entry per column; the check reads the live column list from PostgreSQL's information_schema. Special category data, the Article 9(1) list that includes health and biometric data, must name an Article 9(2) condition and a DPIA. The legal basis is a reviewer's decision; the check only proves someone recorded it.

Configurationpersonal-data.jsonJSON
{
  "app.accounts.email": {
    "category": "contact",
    "purposes": [
      "account",
      "security-notices"
    ],
    "basis": "Art. 6(1)(b) contract",
    "retention": "30 days after account closure"
  },
  "app.accounts.created_at": {
    "category": "account metadata",
    "purposes": [
      "account"
    ],
    "basis": "Art. 6(1)(b) contract",
    "retention": "30 days after account closure"
  },
  "app.checkins.symptoms": {
    "category": "health",
    "purposes": [
      "symptom-tracking"
    ],
    "basis": "Art. 9(2)(a) explicit consent",
    "retention": "2 years after the check-in",
    "dpia": "DPIA-2027-04"
  },
  "app.checkins.user_id": {
    "category": "identifier",
    "purposes": [
      "symptom-tracking"
    ],
    "basis": "Art. 6(1)(b) contract",
    "retention": "2 years after the check-in"
  }
}

What does the inventory check look like?

Run it in CI against a database built from your migrations, so a pull request that adds a column without an inventory entry fails before review. Add the same gate for event schemas and analytics exports, which are where unreviewed copies usually appear.

Testcheck_inventory.pyPython
"""Fail the build when a database column holds data the personal-data inventory doesn't describe.

Usage: psql -At -c "SELECT table_schema || '.' || table_name || '.' || column_name
                    FROM information_schema.columns WHERE table_schema = 'app'" \
         | python3 check_inventory.py personal-data.json
Columns that hold no personal data are listed with "category": "none".
"""

import json
import sys

REQUIRED = ("category", "purposes", "basis", "retention")
# GDPR Art. 9(1) special categories
SPECIAL = {
    "racial or ethnic origin",
    "political opinions",
    "religious or philosophical beliefs",
    "trade union membership",
    "genetic",
    "biometric",
    "health",
    "sex life or sexual orientation",
}

with open(sys.argv[1], encoding="utf-8") as f:
    inventory = json.load(f)
columns = {line.strip() for line in sys.stdin if line.strip()}

problems = []
for column in sorted(columns - inventory.keys()):
    problems.append(f"{column}: not in the inventory; describe it or mark it category none")
for column, entry in sorted(inventory.items()):
    if column not in columns:
        problems.append(f"{column}: in the inventory but not in the database; remove the entry")
    if entry.get("category") == "none":
        continue
    for field in REQUIRED:
        if not entry.get(field):
            problems.append(f"{column}: no {field}")
    if entry.get("category") in SPECIAL:
        if not str(entry.get("basis", "")).startswith("Art. 9(2)"):
            problems.append(f"{column}: special category data needs an Art. 9(2) condition")
        if not entry.get("dpia"):
            problems.append(f"{column}: special category data needs a DPIA reference")

print("\n".join(problems) or f"inventory OK: {len(columns)} columns")
sys.exit(1 if problems else 0)

What does data protection by default look like in a product?

Article 25(2) gives four dials, and each has an engineering default. Amount: make optional fields optional in the API schema and the UI, and reject fields the purpose doesn't need instead of storing them. Extent: keep purposes apart, so data collected for support isn't joined into marketing. Storage period: enforce retention per field with a scheduled job that deletes or anonymizes, and test it, as in /domains/identity-crypto/privacy/data-minimization-retention/. Accessibility: new content, profiles and share links start private, and a public link should expire.

Pseudonymize identifiers that leave the core system, for example with a keyed HMAC whose key stays in the primary environment. That reduces risk for analytics and suppliers, but the data stays personal for you because you hold the key. In September 2025 the Court of Justice held, in EDPS v SRB (C-413/23 P), that pseudonymized data may not be personal for a recipient that has no reasonable means to re-identify anyone, while it remains personal for the controller. The EDPB's pseudonymization guidelines (01/2025) were still a consultation draft on 10 October 2026, and its anonymization guidelines (02/2026) were put out for public consultation from 8 July to 30 October 2026. Techniques are in /domains/identity-crypto/privacy/tokenization-privacy-engineering/.

How do you show Article 32 security of processing?

Article 32 is a risk-based list, so show each item with something that runs:

  • Pseudonymization and encryption: key management configuration (/domains/identity-crypto/key-management-pki/envelope-encryption-rotation/), TLS settings and field-level encryption for special category columns.
  • Ongoing confidentiality, integrity, availability and resilience: access control in code, production access logs and capacity and failover tests.
  • Timely restore: a restore drill with its measured time, and proof that erased subjects stay erased afterwards.
  • Regular testing, assessing and evaluating: penetration test reports, DAST and SAST results and the findings they produced, tracked to closure.
  • Processing only on instructions (Art. 32(4)): role definitions, approvals for production data access and periodic reviews.

When does a feature need a DPIA?

Article 35(1) requires a DPIA before processing that is likely to result in a high risk to people's rights and freedoms. Article 35(3) names three cases that always need one: systematic and extensive automated evaluation, including profiling, with legal or similarly significant effects; large-scale processing of special category or criminal-offence data; and systematic monitoring of a publicly accessible area on a large scale. Each supervisory authority also publishes a list under Article 35(4).

For everything else, the WP248 rev.01 guidelines, endorsed by the EDPB, give nine criteria and a rule of thumb: “In most cases, a data controller can consider that a processing meeting two criteria would require a DPIA to be carried out.” A screen at design review catches this early. The script below takes the feature's flags, set by the engineer and privacy reviewer together, and reports the verdict; one criterion means the reviewer decides, because the guidelines say one can be enough. A DPIA must at least describe the processing, assess necessity and proportionality, assess the risks and set out the measures (Art. 35(7)); if high risk remains, consult the authority first (Art. 36).

dpia_screen.pyPython
"""Screen a feature for a GDPR data protection impact assessment before design sign-off.

The criteria are the nine in the EDPB-endorsed WP248 rev.01 guidelines; Article 35(3) lists
cases that always need one. Your supervisory authority's Article 35(4) list can add more.
Usage: python3 dpia_screen.py feature.json
"""

import json
import sys

ART_35_3 = {
    "automated-evaluation-with-legal-effects": "Art. 35(3)(a)",
    "large-scale-special-category-or-criminal-data": "Art. 35(3)(b)",
    "large-scale-monitoring-of-public-areas": "Art. 35(3)(c)",
}
WP248 = {
    "evaluation-or-scoring",
    "automated-decision-with-legal-or-similar-effect",
    "systematic-monitoring",
    "sensitive-or-highly-personal-data",
    "large-scale",
    "matching-or-combining-datasets",
    "vulnerable-data-subjects",
    "innovative-technology",
    "prevents-exercising-a-right-or-using-a-service",
}


def screen(feature):
    flags = set(feature["flags"])
    unknown = flags - WP248 - ART_35_3.keys()
    if unknown:
        raise ValueError(f"unknown flags: {sorted(unknown)}")
    mandatory = sorted(ART_35_3[f] for f in flags & ART_35_3.keys())
    criteria = sorted(flags & WP248)
    if mandatory or len(criteria) >= 2:
        verdict = "DPIA required before processing starts"
    elif criteria:
        verdict = "privacy review decides; one criterion can be enough"
    else:
        verdict = "no DPIA indicated; check your authority's Art. 35(4) list"
    return {"feature": feature["id"], "verdict": verdict, "art35_3": mandatory, "wp248": criteria}


if __name__ == "__main__":
    with open(sys.argv[1], encoding="utf-8") as f:
        print(json.dumps(screen(json.load(f)), indent=2))

How do you build data subject rights into the system?

Rights requests fail in the stores nobody listed: the search index, the analytics export, the feature store, the support tool. Route every request through one service that reads the inventory's stores, so access (Art. 15) returns everything and erasure (Art. 17) reaches everything. Verify the requester's identity before acting, record the receipt date, and answer within one month (Art. 12(3)). Tell each recipient you disclosed the data to about an erasure or rectification, unless that proves impossible or involves disproportionate effort (Art. 19). For portability (Art. 20), export what the person provided in a structured, commonly used and machine-readable format such as JSON.

Backups are where erased data comes back. Keep a tombstone list of erased subjects and apply it before any restored store serves traffic. The module below does the fan-out, the deadline arithmetic and the tombstones; the test proves a restored backup doesn't resurrect an erased person. The EDPB's Guidelines 01/2022 on the right of access cover what an access response must contain.

subject_requests.pyPython
"""Run access and erasure requests across every store that holds personal data."""

import calendar
from datetime import date


def one_month_after(received: date) -> date:
    """GDPR Art. 12(3): answer within one month of receipt.

    The same day next month, or that month's last day when it is shorter.
    """
    year, month = received.year + received.month // 12, received.month % 12 + 1
    last_day = calendar.monthrange(year, month)[1]
    return received.replace(year=year, month=month, day=min(received.day, last_day))


class SubjectRequests:
    def __init__(self, stores, recipients, tombstones):
        # Every system in the personal-data inventory, including exports, caches and indexes.
        self.stores = stores
        # Parties the data was disclosed to, told about erasure under Art. 19.
        self.recipients = recipients
        # Erased subject IDs, kept to re-apply after a backup restore.
        self.tombstones = tombstones

    def access(self, subject_id, received):
        export = {store.name: store.export(subject_id) for store in self.stores}
        return {"due": one_month_after(received).isoformat(), "data": export}

    def erase(self, subject_id, received):
        removed = {store.name: store.erase(subject_id) for store in self.stores}
        self.tombstones.add(subject_id)
        for recipient in self.recipients:
            recipient.notify_erasure(subject_id)
        return {"due": one_month_after(received).isoformat(), "removed": removed}

    def after_restore(self, store):
        """Run before a restored store serves traffic, so erased subjects stay erased."""
        return sum(store.erase(subject_id) for subject_id in sorted(self.tombstones))

How do you test that erasure really works?

Test against every kind of store you use, with in-memory doubles in unit tests and the real stores in an integration environment. Run python3 -m unittest test_subject_requests.

Testtest_subject_requests.pyPython
import unittest
from datetime import date

from subject_requests import SubjectRequests, one_month_after


class MemoryStore:
    def __init__(self, name, rows):
        self.name, self.rows = name, rows

    def export(self, subject_id):
        return [row for row in self.rows if row["subject"] == subject_id]

    def erase(self, subject_id):
        before = len(self.rows)
        self.rows = [row for row in self.rows if row["subject"] != subject_id]
        return before - len(self.rows)


class Recipient:
    def __init__(self):
        self.notified = []

    def notify_erasure(self, subject_id):
        self.notified.append(subject_id)


class SubjectRequestTest(unittest.TestCase):
    def setUp(self):
        self.db = MemoryStore("postgres", [{"subject": "u1", "email": "a@example.com"}])
        self.export = MemoryStore("analytics-export", [{"subject": "u1", "symptoms": "rash"}])
        self.clinic = Recipient()
        self.requests = SubjectRequests([self.db, self.export], [self.clinic], set())

    def test_deadline_is_one_calendar_month(self):
        self.assertEqual(one_month_after(date(2027, 3, 31)), date(2027, 4, 30))
        self.assertEqual(one_month_after(date(2027, 1, 31)), date(2027, 2, 28))
        self.assertEqual(one_month_after(date(2027, 12, 15)), date(2028, 1, 15))

    def test_access_covers_every_store(self):
        result = self.requests.access("u1", date(2027, 3, 31))
        self.assertEqual(set(result["data"]), {"postgres", "analytics-export"})
        self.assertEqual(result["due"], "2027-04-30")

    def test_erasure_reaches_every_store_and_recipient(self):
        result = self.requests.erase("u1", date(2027, 3, 31))
        self.assertEqual(result["removed"], {"postgres": 1, "analytics-export": 1})
        self.assertEqual(self.clinic.notified, ["u1"])

    def test_restored_backup_does_not_resurrect_erased_subjects(self):
        self.requests.erase("u1", date(2027, 3, 31))
        restored = MemoryStore("postgres", [{"subject": "u1", "email": "a@example.com"}])
        self.assertEqual(self.requests.after_restore(restored), 1)
        self.assertEqual(restored.export("u1"), [])


if __name__ == "__main__":
    unittest.main()

What is changing, and what isn't?

The Commission's Digital Omnibus proposal, COM(2025) 837 of 19 November 2025, would amend the GDPR. It would add to the definition of personal data that information is not personal for an entity that cannot identify the person with means reasonably likely to be used by that entity; notify the authority only of breaches likely to result in a high risk, within 96 hours, through a single entry point; and replace national DPIA lists with EU-wide lists and a common template. It would not change Articles 25 or 32. On 10 October 2026 the proposal was still awaiting a committee vote in the European Parliament and no text had been agreed with the Council, so the current GDPR applies unchanged. Build to the text in force and keep the inventory and DPIA records, which any version will need.

Guided practice

Fernleaf (fictional) is about to launch a symptom check-in feature for its wellness app in the EU. Review the design and the first erasure request against Articles 25, 32 and 35 and list what must change before launch.

Supplied scenario material
Plain text
Feature F-311 "Symptom check-in", Fernleaf wellness app (fictional); about 400,000 monthly users in the EU.
Collects: daily symptoms (checkboxes and free text), mood score, medication names, optional photo of a rash.
Computes: a weekly flare-risk score with a gradient-boosted model; a score above 0.8 sends a push suggesting a GP visit.
Sharing: a user can share a summary with a clinician by link. Draft UI: sharing on by default; links never expire.
Stores: PostgreSQL (primary), OpenSearch (symptom search), daily S3 analytics export keyed by user ID,
        model feature store, nightly backups kept 35 days.
Erasure request E-88: received 2027-03-31 (identity verified the same day).
  2027-04-02 row deleted from PostgreSQL. OpenSearch, analytics export and feature store not touched.
  2027-04-20 a backup restore after an outage brings the account back. Two clinician links still open.
  1. 1. Screen for a DPIA

    Apply Article 35(3) and the WP248 criteria to F-311.

    Expected result and reasoning

    Health data is special category data, and 400,000 users is plausibly large scale, which points to Article 35(3)(b). Separately it meets at least three WP248 criteria: sensitive data, evaluation or scoring (the flare-risk model) and large scale. A DPIA is required before launch; whether the push counts as a significant automated decision is for the DPIA to assess, not a reason to skip it.

  2. 2. Fix the defaults

    List the Article 25(2) problems in the draft design.

    Expected result and reasoning

    Sharing on by default and links that never expire make health data accessible without the user's intervention, which Article 25(2) rules out: start sharing off and expire links. Keep the photo and free text optional and off by default, set retention per field in the inventory, and keep the analytics export pseudonymized with a keyed identifier rather than the user ID.

  3. 3. Record the basis and the security measures

    Say what the inventory and the Article 32 evidence must show.

    Expected result and reasoning

    Every symptom, mood, medication and photo field is health data: the inventory needs an Article 9(2) condition, chosen by the privacy reviewer (for example explicit consent), and the DPIA reference. Article 32 evidence: encryption for those columns and the export, restricted production access with logs, a timed restore drill and test results for the share-link endpoint.

  4. 4. Judge the erasure request

    Work out the deadline for E-88 and what went wrong.

    Expected result and reasoning

    One month from 31 March 2027 is 30 April 2027. Deleting one PostgreSQL row doesn't erase the person: OpenSearch, the export and the feature store still hold the data, and the restore on 20 April brought the account back because no tombstone was applied. The open clinician links are recipients that must be told and revoked (Art. 19). Re-run the erasure through every store, apply tombstones after restores and record it before 30 April.

  5. 5. Write the launch conditions

    List the conditions engineering must meet before F-311 goes live.

    Expected result and reasoning

    Completed DPIA with its measures, private and expiring sharing, inventory entries with Article 9(2) conditions, pseudonymized exports, an erasure service wired to all five stores with tombstones tested against a restore, and the CI inventory check on. Legal basis and the automated-decision question stay with the privacy reviewer.

Your deliverable: A DPIA screening result, five design changes tied to Articles 25 and 32, the E-88 deadline with its failures, and a launch checklist.

Fernleaf and its feature are fictional. The legal basis, controller and processor roles and national DPIA lists need a privacy or legal reviewer; the answers here cover engineering evidence, not legal advice.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. Regulation (EU) 2016/679 (GDPR), Article 25. OJ L 119, 4 May 2016.
  2. GDPR, Article 32: security of processing. OJ L 119, 4 May 2016.
  3. GDPR, Article 35: data protection impact assessment. OJ L 119, 4 May 2016.
  4. GDPR, Article 12: transparent information and time limits for requests. OJ L 119, 4 May 2016.
  5. EDPB Guidelines 4/2019 on Article 25 Data Protection by Design and by Default. Version 2.0 (20 October 2020).
  6. Article 29 Working Party: Guidelines on DPIA, WP248 rev.01 (endorsed by the EDPB). Revised and adopted 4 October 2017.
  7. EDPB Guidelines 01/2022 on data subject rights: right of access. Version 2.1 (17 April 2023).
  8. EDPB Guidelines 01/2025 on pseudonymisation. Version for public consultation, adopted 16 January 2025.
  9. EDPB Guidelines 02/2026 on anonymisation. Version 1.0 for public consultation, adopted 7 July 2026.
  10. Court of Justice press release 107/25: EDPS v SRB, C-413/23 P (4 September 2025)
  11. European Parliament Legislative Observatory: Digital Omnibus, 2025/0360(COD)

Reference information

Download this guide as Markdown