Skip to content
JavaAgentic

Type at least two characters. Try “RAG”, “pgvector” or “tool calling”.

GDPR Compliance for Java Applications

Implementing the parts of GDPR that reach the code: data subject access and export, consent records, erasure through anonymisation, retention jobs, and breach notification.

Intermediate6 min readUpdated
On this page

GDPR is mostly process and legal interpretation, but several obligations land directly in the code: producing everything you hold about a person, deleting it on request, proving consent, and identifying who was affected by a breach. This covers those.

This is engineering guidance, not legal advice — the interpretation of what any obligation requires belongs with your legal or compliance function.

Key Takeaways

  • Build a data map first — you cannot serve rights over data you have not located.
  • Access and portability need a machine-readable export assembled across services.
  • Erasure is often anonymisation, because other law requires retention.
  • Consent must be recorded with timestamp, version and a withdrawal path.
  • Breach tooling must exist before the 72-hour clock starts.

The data map

Every obligation depends on knowing where personal data lives. Record it:

PersonalDataInventory.java
// Annotate fields so the inventory is derivable from code rather than a
// spreadsheet that drifts out of date.
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonalData {
    Category category();          // IDENTITY, CONTACT, FINANCIAL, BEHAVIOURAL
    boolean special() default false;   // special-category data under Article 9
    String retention();           // "7 years", "until account closure"
}
 
@Entity
public class Customer {
    @PersonalData(category = Category.IDENTITY, retention = "until account closure")
    private String fullName;
 
    @PersonalData(category = Category.CONTACT, retention = "until account closure")
    private String email;
 
    @PersonalData(category = Category.FINANCIAL, retention = "7 years (tax law)")
    private String taxId;
}

Deriving the inventory from annotations means a developer adding a personal-data field is prompted to classify it, and the map stays current. A spreadsheet maintained by hand is out of date within a quarter.

Data subject access and portability

Personal data is spread across services, so a subject access request is a fan-out and assemble operation — which only works if you know every service that holds data.
DataExportService.java
@Service
public class DataExportService {
 
    private final List<PersonalDataProvider> providers;   // one per service or domain
 
    public ExportPackage export(String subjectId) {
        // Identity verification happens before this — releasing someone's data
        // to an impersonator is itself a breach.
        var sections = providers.stream()
                .map(provider -> provider.exportFor(subjectId))
                .filter(section -> !section.isEmpty())
                .toList();
 
        auditLog.record("gdpr.export", subjectId);
        // Machine-readable and structured, per the portability right.
        return new ExportPackage(subjectId, Instant.now(), sections);
    }
}
 
// Each service implements this, so a new service that holds personal data
// cannot be forgotten — it either implements the interface or it does not
// hold personal data.
public interface PersonalDataProvider {
    DataSection exportFor(String subjectId);
}

The interface is the mechanism that keeps this correct as the system grows. A new service holding personal data implements it or it does not, and the compiler makes that a visible decision rather than an omission.

Erasure

Erasure is not always deletion, because other legal obligations often require retention:

ErasureService.java
@Service
public class ErasureService {
 
    @Transactional
    public ErasureResult erase(String subjectId) {
        var result = new ErasureResult();
 
        // Data with no retention obligation: hard delete.
        marketingRepository.deleteBySubject(subjectId);
        behaviouralRepository.deleteBySubject(subjectId);
        result.deleted("marketing", "behavioural");
 
        // Data legally required to be retained: anonymise so it no longer
        // relates to an identifiable person, but the record survives for
        // the financial and legal purposes that mandate keeping it.
        orderRepository.findBySubject(subjectId).forEach(order -> {
            order.anonymise();   // name -> "REDACTED", email -> null, address -> region only
            result.anonymised("orders");
        });
 
        // Some data enters a hold rather than being erased: an active dispute,
        // a legal obligation to preserve. Record why.
        if (disputeService.hasActiveDispute(subjectId)) {
            result.retained("orders-in-dispute", "active legal dispute");
        }
 
        auditLog.record("gdpr.erasure", subjectId, result.summary());
        return result;
    }
}

The erasure response must be honest about what was retained and why. Telling a subject their data was deleted when a tax record was anonymised and kept is both inaccurate and itself a compliance problem.

A common pattern is a grace period: soft-delete, wait 30 days in case the request was a mistake or fraudulent, then anonymise or hard-delete. Balance this against the obligation to act "without undue delay".

ConsentRecord.java
@Entity
public class ConsentRecord {
    @Id private UUID id;
    private String subjectId;
 
    @Enumerated(EnumType.STRING)
    private ConsentPurpose purpose;   // ANALYTICS, MARKETING, PERSONALISATION
 
    private boolean granted;
    // The exact wording the subject agreed to, so you can prove WHAT was
    // consented to, not just that something was.
    private String policyVersion;
    private Instant timestamp;
    private String ipAddress;
    private String mechanism;         // "cookie banner", "signup checkbox"
}

Consent must be specific, informed, freely given and withdrawable, and you must be able to prove it. That means recording not just that consent was given but which version of which policy, when, and how — because a policy that changes materially invalidates consent given to the old one.

Withdrawal must be as easy as granting. A one-click unsubscribe, a cookie-preferences page that actually stops the processing, not a support ticket.

Retention

RetentionJob.java
@Component
public class RetentionEnforcementJob {
 
    @Scheduled(cron = "0 0 2 * * *")
    @SchedulerLock(name = "retentionEnforcement", lockAtMostFor = "PT1H")
    public void enforce() {
        // "We keep data as long as necessary" is not a policy unless a job
        // enforces it. Keeping data past its stated retention is a violation
        // even if nobody ever looks at it.
        retentionPolicies.forEach(policy -> {
            var expired = policy.findExpired(Instant.now());
            expired.forEach(record -> {
                archiveIfRequired(record);   // legal hold check first
                policy.erase(record);
            });
            log.info("retention: erased {} {} records", expired.size(), policy.dataType());
        });
    }
}

A retention policy that is written but not enforced is worse than none, because it documents an obligation you are demonstrably not meeting. The scheduled job is what turns the policy into reality.

Breach notification

The 72-hour deadline means the tooling must exist in advance:

BreachAssessment.java
@Service
public class BreachResponseService {
 
    /** Who was affected — the question you must answer within 72 hours. */
    public AffectedSubjects assess(BreachScope scope) {
        return switch (scope.type()) {
            case DATABASE_EXPOSURE -> new AffectedSubjects(
                    subjectRepository.findInTables(scope.affectedTables()),
                    scope.dataCategories());
            case CREDENTIAL_LEAK -> new AffectedSubjects(
                    subjectRepository.findByCredentialSource(scope.source()),
                    Set.of(Category.IDENTITY));
            case LOG_EXPOSURE -> logAnalyser.subjectsInExposedLogs(scope.timeRange());
        };
    }
}

The audit log and the data map are what make this answerable quickly. Without them, "which customers' data was in that table" becomes days of investigation against a 72-hour clock — which is why both need to exist before an incident.

Notification has two audiences: the supervisory authority within 72 hours, and affected individuals "without undue delay" where the risk is high. Prepare templates for both in advance.

Privacy by design

The obligations above are far cheaper to meet when the system was built for them. Collect only the data you need for a stated purpose. Set a retention period at the point of collection, not later. Make the subject id a consistent key across services so a fan-out is possible. And keep an audit trail of access to personal data, because "who looked at this record" is a question that will be asked.

Retrofitting these into a system that scattered personal data without a map is the expensive path, and it is the one most organisations end up on.

What to take away

Build a data map so every obligation has something to operate over. Implement export as a fan-out across services behind a common interface, and erasure as deletion or anonymisation depending on what other law requires. Record consent with its policy version, enforce retention with a job, and build breach-assessment tooling before the 72-hour clock can start.

Frequently Asked Questions

Does erasure mean deleting the row?
Not always, and sometimes it cannot. Financial and tax records must be retained by law, so erasure there means anonymisation — replacing identifying fields so the record no longer relates to an identifiable person while preserving the aggregate. A hard delete is right for data with no retention obligation.
How do I export everything for a subject access request?
In a microservices system, personal data is spread across services, so a coordinator queries each one for data relating to the subject and assembles a machine-readable package. The hard part is the inventory — knowing every place a person appears, which is exactly what a data map records.
What is the 72-hour rule?
A personal data breach likely to risk individuals must be reported to the supervisory authority within 72 hours of becoming aware of it. That deadline means the detection, assessment and affected-user-identification tooling has to exist before an incident, not be built during one.

Related tutorials