Skip to content
Francesco Bilotta
Compliance as code

GDPR Compliance as Code: Automating ROPA and DPIA in Spring Boot

Kill the DPO's spreadsheet. Annotate the domain types and let the build generate the ROPA, the DPIA, and the erasure path.

Francesco Bilotta · · 4 min read

There is a spreadsheet, and everyone knows the spreadsheet is lying. The Data Protection Officer maintains it by hand. It lists every category of personal data the system processes, the lawful basis for each, the purpose, the retention period, who the data subject is. This is the Record of Processing Activities, the ROPA, and Article 30 of the GDPR makes it mandatory. Next to it sits the Data Protection Impact Assessment, the DPIA that Article 35 requires wherever processing is high risk. Both are documents. Both are maintained beside the code, never inside it.

So a developer adds a column. Someone starts logging an IP address for debugging, or a new integration begins storing a phone number for a feature nobody looped the DPO in on. The spreadsheet does not know. Within a sprint the document has drifted from the running system, and the gap widens with every commit, silently, because nothing connects the two. Then the audit arrives, or worse, the breach, and the question is the one no hand-maintained register can answer: prove that this document describes what the system actually does. Nobody can. The map was drawn once and the territory kept moving.

This is not a discipline problem you fix by nagging the DPO to update the spreadsheet more often. It is an architecture problem. The register is derived from the system, so it must be derived from the code, mechanically, on every build, or it will drift. Compliance is architecture, not a spreadsheet.

The declaration belongs on the type that holds the data

The facts the ROPA needs are not abstract policy. They are properties of specific fields on specific domain types. This customer record holds an email, the lawful basis is contract, the purpose is order fulfilment, the retention is ten years for tax law. Every one of those facts is true of a field in the code. The only reason they live in a separate spreadsheet is that, historically, the code had no way to state them.

Give the code that way, and the declaration moves to where the data actually is:

@ProcessingActivity(
    dataSubject = DataSubject.CUSTOMER,
    lawfulBasis = LawfulBasis.CONTRACT,
    purpose = "Order fulfilment and invoicing",
    retention = "P10Y" // tax law
)
public class CustomerRecord {

    @PersonalData(category = Category.CONTACT)
    private String email;

    @PersonalData(category = Category.CONTACT)
    @Erasable
    private String phoneNumber;
}

Now the declaration cannot drift, because it is attached to the field it describes. Rename the field, delete it, add a new one holding a passport number, and the annotation moves, dies, or is demanded with it. The compiler participates. A @PersonalData field with no lawful basis on its type is no longer an oversight buried in a document review six months late; it is a build that fails today.

The build generates the register

spring-gdpr is a Spring annotation processor. It reads these annotations at build time, from the actual domain types the application compiles and ships, and emits the compliance artefacts as a product of the build. From the annotations above it generates:

  • the ROPA entry for CustomerRecord: subject, basis, purpose, retention, data categories, all of Article 30 assembled from the code that holds the data.
  • the DPIA input for high-risk fields, so the Article 35 assessment starts from a true inventory instead of a best guess.
  • an audit log of processing, wired in rather than remembered.
  • the right-to-erasure path for @Erasable fields, so Article 17 is a code path the build knows about, not a promise in a policy PDF.

This is what Article 25, Privacy by Design, actually asks for when you take it literally. Not a clause in a contract and a training deck. Data protection built into the processing itself, at the moment of design, expressed in the type system. The register is generated from the code that touches the data, so it can no longer diverge from what the system does. It is derived from reality, and reality is the thing an auditor gets to inspect.

The DPO stops maintaining a spreadsheet and starts reviewing a generated artefact. That is a different job, and a better one. The human decides whether the declared basis is the right basis, whether the retention is defensible, whether a processing activity should exist at all. The human stops doing the bookkeeping the machine should have been doing, and the bookkeeping stops being wrong.

Where this comes from

I built these controls in production, in a regulated domain, where the audit evidence had to be generated automatically because a hand-kept version would have been out of date the day it was filed. The pattern held, so I extracted it and open-sourced it as spring-gdpr, under Apache 2.0, with a companion library spring-aiact that does the same trick for the EU AI Act, and an end-to-end demo running both together. GDPR compliance by annotation: annotate the domain types, get the audit log, the erasure path, the DPIA and the ROPA at every build.

The register can no longer drift from reality, because it is derived from reality. That is the whole idea. Everything else is a spreadsheet waiting to be caught out.