Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Life Cycle Callbacks in JPA: Where Behavior Belongs

Posted on September 1, 2008 By Luis Fernandez

Life cycle callbacks in JPA look tiny on paper. A couple of annotations and a method that runs at the right moment. In practice they answer a bigger question that teams ask every week: where does behavior belong? I keep seeing projects split between a fat service layer that knows too much about entities and a domain model that is only getters and setters. Then the team ships, a rule changes, and they need to hunt down the five places that update the same fields. Not fun.

Right now a lot of us are using JPA 1 with Hibernate EntityManager, TopLink Essentials or OpenJPA under Spring or in an EJB 3 container. We are wiring Tomcat or GlassFish and trying to keep things simple. JPA gives us a quiet but powerful hook: @PrePersist, @PostPersist, @PreUpdate, @PostUpdate, @PreRemove, @PostRemove, @PostLoad. With those you can place behavior with the entity so that it always runs at the right time. No XML gymnastic. No filters you forget to register.

Problem framing

Most teams I talk to share the same pain. They need to fill audit fields, normalize data, enforce rules, and sometimes do a soft delete. They start with helpers and util classes. Then a copy paste party begins. The code gets scattered. Someone forgets to set updatedAt on one path and a report is wrong. You can push these rules into the service layer, but it is easy to bypass if another service writes the same entity.

Life cycle callbacks are the escape hatch that brings those rules back to the model. When an entity is about to be persisted or updated you can make sure it is clean and consistent. When it is removed you can flip a flag instead of losing data. When it is loaded you can rebuild derived fields. And because the hooks live next to the entity, the rule is not scattered.


Three case walkthrough

Case 1. Auditing that never forgets

We all need created at and updated at. Often created by and updated by too. The trick is to set them in one reliable place. Put the rule in a mapped super class and let all entities inherit it. Use @PrePersist and @PreUpdate.

import javax.persistence.*;
import java.util.Date;

@MappedSuperclass
public abstract class AuditedEntity {

    @Column(name = "created_at", nullable = false, updatable = false)
    private Date createdAt;

    @Column(name = "updated_at", nullable = false)
    private Date updatedAt;

    @Column(name = "created_by", updatable = false)
    private String createdBy;

    @Column(name = "updated_by")
    private String updatedBy;

    @PrePersist
    protected void onCreate() {
        Date now = new Date();
        this.createdAt = now;
        this.updatedAt = now;
        String user = CurrentUser.get(); // ThreadLocal holder set by a filter or interceptor
        this.createdBy = user;
        this.updatedBy = user;
    }

    @PreUpdate
    protected void onUpdate() {
        this.updatedAt = new Date();
        this.updatedBy = CurrentUser.get();
    }

    // getters and setters...
}

Now any entity that extends AuditedEntity will always set those fields. No service needs to remember it. This works the same with Hibernate, TopLink Essentials and OpenJPA in my tests this week on Tomcat and GlassFish. Keep the CurrentUser holder simple. A servlet filter can set the username at the start of the request and clear it at the end.

Case 2. Validation and normalization that travel with the entity

We often want to enforce local rules like email format or a price being positive. We also want to normalize, like trimming spaces. You can run this in setters, but then bulk updates or field changes that skip setters might bypass it. A small callback keeps it honest.

import javax.persistence.*;
import java.math.BigDecimal;

@Entity
public class Customer {

    @Id @GeneratedValue
    private Long id;

    private String email;

    private BigDecimal creditLimit;

    @PrePersist
    @PreUpdate
    private void validateAndNormalize() {
        if (email != null) {
            email = email.trim().toLowerCase();
        }
        if (email == null || !email.contains("@")) {
            throw new IllegalStateException("Email is required and must contain @");
        }
        if (creditLimit == null) {
            creditLimit = BigDecimal.ZERO;
        }
        if (creditLimit.signum() < 0) {
            throw new IllegalStateException("Credit limit cannot be negative");
        }
    }

    // getters and setters...
}

This rule is local to the entity and will run on both persist and update. If you prefer to reuse existing validators, Hibernate Validator plays nice here. You can call a static Validator inside the callback. The key is the same: the rule sits with the data it protects.

Case 3. Soft delete without drama

Delete can be dangerous. Many teams choose a soft delete and keep the record. In pure JPA you cannot stop the remove once it is called in a way that flips it to an update automatically, but you can handle it with a flag and a small convention.

import javax.persistence.*;
import java.util.Date;

@Entity
public class Post {

    @Id @GeneratedValue
    private Long id;

    private String title;

    private boolean deleted;

    @Temporal(TemporalType.TIMESTAMP)
    private Date deletedAt;

    @PreRemove
    private void onRemove() {
        // mark as deleted so listeners or the service can switch strategy
        this.deleted = true;
        this.deletedAt = new Date();
    }

    public boolean isDeleted() { return deleted; }

    // other fields and methods...
}

How to apply it day to day. Instead of calling em.remove(post), expose a domain method or a repository method that calls post.onRemove() and then em.merge(post). The @PreRemove hook still helps if someone forgets and calls remove, at least the entity knows it was marked. For filtering you can add WHERE deleted = false to your queries or wrap it in a repository method so callers do not forget. If you use Hibernate you can also add a clause with an annotation on the entity, but the repository approach stays portable across providers.

There is also a neat use for @PostLoad. You can restore derived fields or cache computed values after the entity is fetched without persisting them. Keep it simple and do not trigger extra queries inside the callback.

@PostLoad
private void afterLoad() {
    // Example: build a display name
    this.displayName = firstName + " " + lastName;
}

Objections and replies

Objection: All business logic belongs in services. Entities should be simple.

Reply: Keep cross entity workflows in services. Keep local invariants in the entity. If a rule only touches the fields of the entity, put it in a callback or a method the callback calls. That way every save respects it. Your services get thinner and your model gains meaning.

Objection: Callbacks hide behavior and surprise teammates.

Reply: Surprise comes from silence. Make callbacks small, named and visible. Use names like validateAndNormalize or onCreate. Keep them in the entity class or in an @EntityListeners class next to it. Add a short note in the class javadoc. During code review, look for these annotations. The point is not to be clever, the point is to make a promise the app can keep.

Objection: Testing callbacks is painful without a container.

Reply: Most callback logic can be tested by calling the method directly. It is just Java. For a full round trip, an in memory database like HSQLDB with Hibernate or OpenJPA takes minutes to wire in a test. If you are on Spring, @Transactional tests work great here. You do not need GlassFish or JBoss running for unit tests.

Objection: Callbacks can be slow or load lazy stuff by mistake.

Reply: Treat callbacks like code on a hot path. Keep them fast. Do not fetch associations. Do not call other repositories. Avoid network calls. If you need cross entity checks then do that in the service before you call persist or merge. Use callbacks for local transformations and guards.

Objection: Portability is shaky across providers.

Reply: The core annotations are part of JPA and behave the same across Hibernate, OpenJPA and TopLink in normal cases. What gets tricky are provider extras like query filters or custom delete SQL. Keep those at the edges. If you stay with the JPA life cycle annotations and plain Java code, you stay in the safe zone.


Action oriented close

Here is a short plan you can pick up this week and apply to your codebase.

  • Write down your local invariants. For each entity, list the rules that touch only its own fields. Those are callback material.
  • Create a mapped super class for audit fields and put the @PrePersist and @PreUpdate logic there. Make two entities extend it and remove any audit code from services.
  • Add a validation method to a core entity and annotate it with @PrePersist and @PreUpdate. Keep it small and readable. Throw clear exceptions.
  • Decide on delete behavior. If you need soft delete, add a flag and a convention in your repository methods to filter it out. Only use provider specific extras if you can keep them in one place.
  • Keep callbacks side effect free. No remote calls. No extra queries. Just make the entity right.
  • Test directly. Call the callback method in a unit test. Then add one integration test with an in memory database to cover the wiring.
  • Document your callbacks in the class header. A two line note beats a surprise during a late night deploy.

And for quick reference, here is the tiny cheat sheet I keep near my editor.

  • @PrePersist: before insert. Great for defaults, audit, normalize.
  • @PostPersist: after insert. Mostly for debugging or id based work that stays in memory.
  • @PreUpdate: before update. Recheck validations and update audit.
  • @PostUpdate: after update. Keep it light.
  • @PreRemove: before delete. Mark soft delete or veto in your own repository flow.
  • @PostRemove: after delete. Often empty in a pure domain model.
  • @PostLoad: after fetch. Rebuild transient fields. No extra queries.
  • @EntityListeners: move shared rules to a separate class if you do not want to inherit from a base class.

The point is simple. Put behavior where it belongs. If a rule lives in the entity, let it live there and let JPA call it at the right moment. Your services will breathe. Your model will speak. And the next time someone asks where updatedAt is set, you will point to one place and go back to real work.

If you have stories from running this on JBoss or GlassFish, or tricks with OpenJPA or TopLink Essentials, send them over. I will try them on my test project and share results. Today we all learn from each other, and life cycle callbacks are one of those small features that pay rent every day.

Software Architecture Software Engineering

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes