GORM Event Listeners for Centralized Grails Auditing
How GORM event listeners let you capture domain changes audits without cluttering controllers with a concrete listener example and verification steps.
27 Feb 2026, 00:20 UTC

The problem & takeaway
\nIn enterprise Grails applications compliance often requires knowing who changed what and when. Scattering audit logic across controllers and services quickly becomes unmaintainable. GORM event listeners provide a framework-native hook that centralizes this concern keeping domain classes focused on business rules while capturing change metadata automatically.
\nHow GORM events work
\nGORM fires lifecycle events such as beforeInsert beforeUpdate and afterDelete during persistence operations. Each event gives you access to the domain instance its identifier and a map of fields that changed between their original and current values the internal snapshot GORM maintains per request allowing you to reconstruct what changed without extra instrumentation.
- \n
- You can define a listener once and register it per package in configuration so the same audit logic applies across any domain that needs tracking. \n
- Because the listener runs within the same transaction as the domain change audit records are committed only when the operation succeeds preventing orphaned entries. \n
Concrete example
\npackage com.example.listeners
class BookAuditListener {
def auditService
boolean beforeInsert(final Object target) {
audit(target, null)
return true
}
boolean beforeUpdate(final Object target, final Serializable oldID) {
audit(target, oldID)
return true
}
private void audit(final Object target, final Serializable oldID) {
def changes = target.dirtyProperties
if (changes) {
auditService.logChanges(target.class.simpleName, target.id, oldID, changes)
}
}
\nRegister the listener in grails-app/conf/application.yml:\n
grails:\n gorm:\n packages:\n - com.example.listeners.BookAuditListener\n
The auditService is injected by Grails dependency injection; you can pull the current user from Spring Security context inside that service without touching the domain.
Trade-offs and verification
\nBecause the listener runs synchronously each update carries the cost of writing an audit row. For high-throughput paths measure request latency with and without the listener using Grails metrics endpoints. If the overhead is unacceptable consider offloading audit writes to a background job or queue.
\nTransaction safety is a built-in benefit: if the domain save throws an exception after the listener runs the entire transaction rolls back and no audit record persists. You can verify this by wrapping a save in a try/catch and checking that the audit table remains empty for that operation.
\n- \n
- Start by adding an audit listener to one critical domain run your test suite and query the audit table to confirm records contain the expected changes old and new values and timestamps. \n
- Wrap a domain save in a transaction that rolls back and verify no audit record persists confirming the listener respects transaction boundaries. \n
Actionable next steps: expand the pattern to other domains and evaluate whether a queued approach better fits your performance profile.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.