Most teams treat HIPAA's technical safeguards as a checklist bolted onto each service. There is a better place for them: the platform itself. Here is the pattern — and a small open-source Spring Boot starter that makes encryption, audit, and access control inherited behavior, from a single annotation.
Ask an engineering team how they handle Protected Health Information and you usually get a checklist: a Confluence page, a security review, a promise that the next service will encrypt the sensitive columns and log who reads them. It works for the first service. By the tenth, PHI protection is a patchwork — some fields encrypted, some not; audit logging in three different shapes; access control that depends on whoever wrote that endpoint remembering to add it. Every new service is a fresh chance to get it wrong, and a fresh thing for a reviewer to catch.
There is a more durable approach, borrowed from how good platform teams handle cross-cutting concerns generally: make the safe thing the default thing. Instead of asking every service to remember to protect PHI, you make protection an inherited property of the platform — so a developer gets it for free, and has to go out of their way to not have it. This post is about doing exactly that for three of HIPAA's technical safeguards, with a concrete, runnable reference you can read.
What HIPAA §164.312 actually asks for
The HIPAA Security Rule's technical safeguards live in 45 CFR §164.312, and they are more concrete than the reputation suggests. Five standards: access control, audit controls, integrity, person-or-entity authentication, and transmission security. Three of those map cleanly onto things a platform can guarantee at the data layer:
- Access control (§164.312(a)) — only authorized people touch ePHI; includes an encryption/decryption implementation specification.
- Audit controls (§164.312(b)) — record and examine activity in systems that contain ePHI.
- Integrity (§164.312(c)) — protect ePHI from improper alteration, with a mechanism to prove it hasn't been tampered with.
A note that trips people up: some specs are labelled "addressable" rather than "required" (encryption is one). Addressable does not mean optional. It means you either implement it as reasonable and appropriate, or document an equivalent compensating control based on a risk assessment. For most systems holding real PHI, "we encrypt it" is by far the easiest position to defend — so the pragmatic move is to make encryption free enough that there's no reason to skip it.
The idea: safeguards as inherited platform behavior
The design goal is one sentence: a developer annotates a field @Phi, and that field is encrypted at rest, every read of it is audited, and access to it is limited to the roles that need it — without writing any of that logic. The compliance controls become libraries the whole platform inherits, not code each service re-implements.
We packaged this as a small open-source Spring Boot starter (Kotlin) so it's concrete rather than hand-wavy. Add one dependency, set an encryption key, annotate your entity — and the three safeguards below switch on. The rest of this post walks each one and shows the mechanism.
Safeguard 1 — PHI field encryption at rest
The unit of protection is the field, not the database. Full-disk or tablespace encryption is good hygiene, but it protects against a stolen disk, not against a leaked backup, an over-broad query, or a log line that accidentally prints a row. Field-level encryption means the SSN is ciphertext everywhere it is at rest — in the column, in the replica, in the nightly dump — and only becomes plaintext inside the running application, for code that is allowed to see it.
The ergonomics matter, because ergonomics are what make it actually get used. A field is marked with a single annotation:
@Entity
@EntityListeners(PhiEncryptionListener::class)
class Patient(
@field:Phi var name: String = "",
@field:Phi var ssn: String = "",
@field:Phi var dateOfBirth: String = "",
var mrn: String = "", // not PHI — stored in the clear
)
(The @field: use-site target matters in Kotlin: without it the annotation would land on the constructor parameter, and the listener that reflects over fields would never see it — a small correctness detail the starter gets right.)
A JPA entity listener does the work. On @PrePersist and @PreUpdate it reflects over the fields annotated @Phi and replaces each value with its ciphertext; on @PostLoad it decrypts them back. The cipher is AES-256-GCM — authenticated encryption, so a tampered ciphertext fails to decrypt rather than returning garbage — with a random 12-byte IV per value, the whole thing Base64-encoded as Base64(IV ‖ ciphertext ‖ tag). The application code never calls encrypt(); the field simply is protected.
The one part you must take seriously in production is the key. The starter reads a key from configuration for demonstration, behind a KeyProvider interface — and the entire point of that interface is that in a real deployment you back it with a KMS or Vault, so the key material lives in a managed store with its own audit trail and rotation, never in a config file next to the data it protects. That is the difference between "encrypted" and "encrypted in a way that means something."
Safeguard 2 — an immutable, tamper-evident audit trail
§164.312(b) wants you to record activity against ePHI. The naive version — an audit_log table anyone with database access can UPDATE — technically produces logs, but it doesn't produce trustworthy logs. If an insider can quietly edit or delete the record of what they read, the audit trail is theater.
The starter records access declaratively — a service method is marked @AuditPhiAccess and an aspect writes an AuditEvent (who, what action, which resource, when) every time it runs — and then makes the trail tamper-evident by chaining the entries. Each event carries the hash of the one before it:
hash = SHA-256( seq | timestamp | actor | action | resourceType | resourceId | prevHash )
Because every entry commits to its predecessor, the log becomes an append-only chain: editing or deleting any past event changes its hash, which breaks the prevHash link of every event after it. A verifyChain() check walks the chain and returns false the moment anything has been altered. This is the same primitive a blockchain uses, applied to a much humbler and more useful problem — and it satisfies the integrity standard (§164.312(c)) at the same time, because the chain is the mechanism that authenticates the audit record hasn't been improperly altered. Who read Patient/8842, and when, becomes provable, not merely logged.
Safeguard 3 — minimum-necessary access
Access control (§164.312(a)(1)) is where "minimum necessary" lives: a person should reach only the PHI their role actually requires. In practice this is the safeguard most often scattered as ad-hoc if (user.role == ...) checks sprinkled through controllers — easy to forget, hard to audit, impossible to reason about globally.
The starter turns it into a declaration on the method that touches PHI:
@AuditPhiAccess(action = "read", resourceType = "Patient")
@MinimumNecessary("clinician", "admin")
fun getById(id: Long): Patient =
repository.findById(id).orElseThrow { NoSuchElementException("Patient $id not found") }
An aspect checks the caller's roles (from a pluggable access context — a thread-scoped abstraction you can back with Spring Security or your own gateway) against the roles the method requires, and throws before the method body runs if there's no overlap. The access rule sits on the operation, in one line, where a reviewer can see it — instead of being buried in branching logic. And because it's declarative, the same annotation that gates access is the natural place the audit record is stamped, so "who is allowed" and "who actually did" stay in lockstep.
What this is, and what it isn't
This is a set of building blocks, not a compliance program. Encryption at rest, tamper-evident audit, and minimum-necessary access are three of the technical safeguards — they do not, by themselves, give you person-or-entity authentication, transmission security (TLS everywhere), automatic logoff, a risk analysis, business-associate agreements, or the administrative and physical safeguards the rest of the Security Rule requires. The starter is deliberately honest about that in its README. What it does give you is the part that's genuinely reusable as code — the part that, left to per-service discipline, is where real systems quietly drift out of compliance.
See it in code — it's open source
The reference implementation is open source: a Kotlin Spring Boot starter with the @Phi field encryption, the hash-chained audit trail, and the @MinimumNecessary access aspect, wired as Spring Boot auto-configuration — plus a runnable demo app whose tests prove the guarantees (they persist a patient, query the raw column to confirm it's ciphertext, verify the audit chain, and assert that an under-privileged caller is denied). Read it, run it, or lift the pieces you need: github.com/Nirmitee-tech/hipaa-spring-boot-starter.
It's the third in a set of small, standards-first healthcare building blocks we've open-sourced — alongside an anti-corruption layer that maps X12 837/835 to FHIR and a prior-authorization workflow engine. Different problems, same philosophy: make the correct, compliant thing the default thing.
Where we come in
We build FHIR-native, standards-first healthcare platforms — and the unglamorous foundations underneath them, including the paved roads that make PHI handling safe by default across a whole system rather than one service at a time. If you're carrying PHI across a growing set of services and want that to be a platform guarantee instead of a per-team checklist, let's talk.
