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.
| Setting | Value | Note | Source |
|---|---|---|---|
| Answer a subject request | Without undue delay and within one month of receipt | Extendable by two further months for complex or numerous requests; tell the person within the first month, with reasons. | GDPR Art. 12(3) |
| Data portability format | Structured, commonly used and machine-readable | Applies to data the person provided, processed by automated means on consent or contract. | GDPR Art. 20(1) |
| When a DPIA is required | Before processing likely to result in a high risk; always for the Art. 35(3) cases | WP248: in most cases when two of the nine criteria are met. | GDPR Art. 35(1), 35(3); WP248 rev.01 |
| Prior consultation | Before processing, when the DPIA shows high residual risk | The authority advises within eight weeks, extendable by six. | GDPR Art. 36(1), 36(2) |
| Breach notice to the authority | Within 72 hours of becoming aware, unless unlikely to result in a risk | The Digital Omnibus proposal would change this to high-risk breaches within 96 hours; not adopted. | GDPR Art. 33(1) |
| Default sharing and visibility | Not accessible to an indefinite number of people without the person's intervention | — | GDPR Art. 25(2) |
- Add a column in a migration without an inventory entry and confirm the CI check fails.
- 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.
- 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.
- Restore last night's backup into a test environment and confirm erased subjects are removed before the store serves traffic.
- 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 prerequisites
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 practiceThree 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.
| Article | What it requires | What engineering builds and keeps |
|---|---|---|
| 25(1) by design | Appropriate technical and organizational measures, such as pseudonymization, designed to implement the principles, such as data minimisation, both when the means are chosen and during processing | Privacy requirements in design review, a personal-data inventory, pseudonymized identifiers in analytics, deletion built into each store |
| 25(2) by default | Only the data necessary for each purpose: amount collected, extent of processing, storage period and accessibility | Optional 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 testing | Encryption configuration, access reviews, restore drills with timings, penetration and DAST results |
| 32(4) | People with access process personal data only on the controller's instructions | Role-based access with logged, reviewed production access |
| 35 | A DPIA before high-risk processing: description, necessity and proportionality, risks and the measures to address them | A screen at design review, the DPIA record, and a link from each high-risk field to its DPIA |
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.
{
"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"
}
}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.
"""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)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/.
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.
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).
"""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))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.
"""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))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.
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()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
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. 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. 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. 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. 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. 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.
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.
- Regulation (EU) 2016/679 (GDPR), Article 25. OJ L 119, 4 May 2016.
- GDPR, Article 32: security of processing. OJ L 119, 4 May 2016.
- GDPR, Article 35: data protection impact assessment. OJ L 119, 4 May 2016.
- GDPR, Article 12: transparent information and time limits for requests. OJ L 119, 4 May 2016.
- EDPB Guidelines 4/2019 on Article 25 Data Protection by Design and by Default. Version 2.0 (20 October 2020).
- Article 29 Working Party: Guidelines on DPIA, WP248 rev.01 (endorsed by the EDPB). Revised and adopted 4 October 2017.
- EDPB Guidelines 01/2022 on data subject rights: right of access. Version 2.1 (17 April 2023).
- EDPB Guidelines 01/2025 on pseudonymisation. Version for public consultation, adopted 16 January 2025.
- EDPB Guidelines 02/2026 on anonymisation. Version 1.0 for public consultation, adopted 7 July 2026.
- Court of Justice press release 107/25: EDPS v SRB, C-413/23 P (4 September 2025)
- European Parliament Legislative Observatory: Digital Omnibus, 2025/0360(COD)
Reference information
- SDLC phase
- PlanningDesignSecurity program