How to implement field-level AES-256-GCM encryption in Spring Boot (and why we packaged it into one annotation)
If you've ever had to encrypt a nationalId , a creditCardNumber , or a medicalRecord field in a Spring Boot entity, you already know the drill. You write an AttributeConverter , you wire up a Cipher instance, you generate an IV, you figure out where the key lives, you get the GCM tag handling wrong once, you fix it, and three weeks later you finally trust it enough to ship. We've done this enough times — across healthcare and fintech projects — that we stopped doing it manually. This post walks through the full implementation from scratch, the mistakes that are easy to make along the way, and then shows the one-annotation version we eventually packaged into Nucleus , our open-core Java framework. Why GCM, and not just AES-CBC If you search "AES encryption Java" you'll find a lot of CBC-mode examples. Don't use them for new code. CBC gives you confidentiality but no integrity check — an attacker can flip bits in the ciphertext and you won't know it happened until something downstream breaks in a weird way, or worse, doesn't break at all. GCM (Galois/Counter Mode) gives you both confidentiality and authentication in one pass. It produces an authentication tag alongside the ciphertext, and decryption fails loudly if either the ciphertext or the tag has been tampered with. It's also the mode behind TLS 1.3, which is a reasonable signal that it's held up to scrutiny. The relevant specification is NIST SP 800-38D. Building it by hand Here's a minimal, correct implementation. This is the version you'd write before you have a framework to lean on. public class AesGcmEncryptor { private static final String ALGORITHM = "AES/GCM/NoPadding" ; private static final int GCM_TAG_LENGTH_BITS = 128 ; private static final int GCM_IV_LENGTH_BYTES = 12 ; private final SecretKey key ; public AesGcmEncryptor ( SecretKey key ) { this . key = key ; } public String encrypt ( String plaintext ) { try { byte [] iv = new byte [ GCM_IV_LENGTH_BYTES ]; SecureRandom . getInstanceStrong (). nextByt