A production support ticket comes in:
"Why was this account deactivated?"
You check the database:
id = 1042
status = DEACTIVATED
Then you search the application logs:
2026-10-12T09:42:18Z INFO AccountService
Account 1042 updated successfully
Great. The application updated the account.
But that doesn't answer the question.
What was the previous status? Who initiated the change? Did the update actually commit? Was this the first transition, or had the account changed several times?
This is a distinction that becomes increasingly important when supporting production applications:
Application logs describe execution. Audit trails describe changes to business data.
They are related, but they aren't interchangeable.
1. A Log Message Isn't Proof of a Committed Change
Consider a typical Spring Boot service:
@Service
@RequiredArgsConstructor
public class AccountService {
private final AccountRepository accountRepository;
@Transactional
public void deactivate(Long accountId) {
Account account = accountRepository.findById(accountId)
.orElseThrow(() -> new AccountNotFoundException(accountId));
account.setStatus(AccountStatus.DEACTIVATED);
log.info("Account {} deactivated", accountId);
validateAdditionalRules(account);
}
}
Suppose validateAdditionalRules() throws an unchecked exception.
By default, Spring rolls back the transaction.
But the log message has already been emitted.
You might end up with:
APPLICATION LOG
09:42:18 Account 1042 deactivated
DATABASE
Account 1042
status = ACTIVE
The log is not necessarily incorrect. It tells us that execution reached the logging statement.
But it does not prove the database change was committed.
This is one of the most important reasons not to treat application logs as authoritative business history.
What about logging after commit?
You could use Spring transaction synchronization to log only after a successful commit.
That addresses the premature-success-message problem, but it introduces another limitation.
What happens if the database commits successfully and the process crashes before the log is delivered?
The business change exists, but the corresponding log message might not.
Moving logging after commit does not make the database update and log delivery atomic.
For important business-data history, we need a different persistence guarantee.
2. Hibernate Envers Provides Transactional Revision History
Hibernate Envers is a natural fit for applications using Hibernate ORM.
Instead of manually logging every entity modification, we can configure entities for auditing.
For example:
@Entity
@Audited
@Table(name = "accounts")
public class Account {
@Id
private Long id;
@Enumerated(EnumType.STRING)
private AccountStatus status;
private String reasonCode;
}
With Envers configured, changes made through Hibernate's audited persistence lifecycle can produce corresponding revision records.
Those records are normally persisted within the same database transaction as the entity modifications.
Conceptually:
BEGIN TRANSACTION
|
+-- Update Account
|
+-- Persist Audit Revision
|
COMMIT
If the transaction rolls back, the entity modification and associated audit revision roll back together.
That gives us an important property:
The audit revision is tied to the committed business-data change.
There are still limitations.
Direct SQL modifications outside Hibernate, unsupported write paths, incorrect mappings, and incomplete revision metadata can compromise audit completeness.
Envers is not a universal database change-data-capture mechanism.
But for audited Hibernate-managed entities, it provides a solid foundation.
3. Logs and Audit Trails Have Different Data Models
Consider these application logs:
09:41:12 Account update requested
09:41:12 Validating account 1042
09:41:12 Validation successful
09:41:13 Updating account 1042
09:41:13 Account updated successfully
They help us reconstruct the execution.
Now compare that with an audit history:
Account #1042
Revision 18
2026-10-12T09:41:13Z
status:
ACTIVE -> SUSPENDED
reasonCode:
null -> CUSTOMER_REQUEST
changedBy:
admin-17
The second representation is organized around the business entity.
It provides a history of what changed, rather than a sequence of messages describing application execution.
Over time, the audit history might look like:
Oct 12
ACTIVE -> SUSPENDED
Oct 15
SUSPENDED -> ACTIVE
Oct 21
ACTIVE -> DEACTIVATED
That is much easier to consume when someone asks:
How did this account reach its current state?
It also opens the door to building administrative interfaces and support tools around revision history.
4. Why Not Just Log Every Field Change?
It's possible to write structured logs containing old and new values.
For some applications, that may be sufficient.
But there are several challenges.
Consistency: Every write path must follow the same logging convention, including scheduled jobs, message consumers, and administrative operations.
Transaction boundaries: Logging a change does not necessarily prove the corresponding transaction committed.
Retention: Application logs may be deleted long before business history is no longer needed.
Queryability: Searching logs is not the same as querying entity revisions, changed properties, and historical values.
Security: Logging complete entity snapshots can expose sensitive information.
These concerns deserve explicit architectural decisions.
If historical business-data changes are important, they should not depend entirely on individual developers remembering to add the correct log statements.
5. Audit History Still Doesn't Explain Everything
Suppose we have the following revision:
Account #1042
status:
ACTIVE -> SUSPENDED
changedBy:
account-service
changedAt:
2026-10-12T09:41:13Z
We know what changed.
But do we know why?
Not necessarily.
Hibernate Envers cannot automatically infer the business intent behind a state transition.
To make that possible, the application needs to capture relevant context.
For example:
reasonCode:
CUSTOMER_REQUEST
correlationId:
c5d8f1e2-7a40-4d9c-8b1e-9f1c02d5a611
The reason code provides explicitly recorded business context.
The correlation ID may allow us to connect the revision to related application logs and distributed traces, provided the identifier was propagated consistently.
This is where audit history and observability complement each other.
Audit history tells us what changed. Logs and traces help investigate the execution that produced the change.
Neither should invent information that was never captured.
6. Distributed Systems Need Both Event History and Audit History
Consider a simple asynchronous workflow:
Account API
|
v
Account Service
|
v
Database
|
v
Transactional Outbox
|
v
Kafka
|
v
Notification Service
Suppose an account is suspended.
Several things may happen:
- The account status changes from
ACTIVEtoSUSPENDED. - An audit revision records the state change.
- An outbox event is persisted.
- The transaction commits.
- The event is published asynchronously.
- A downstream consumer processes the event.
Each step raises different operational questions.
Was the account state committed?
Was the event persisted?
Was the event published?
Did the consumer process it?
Was the notification delivered?
A single logging mechanism should not be expected to answer all these questions.
This is also where two of my open-source libraries complement each other.
NERV Event: What Happened to the Message?
NERV Event focuses on reliable event-driven processing through patterns such as:
- Transactional outbox
- Inbox processing
- Retries
- Idempotency
These capabilities help applications reason about asynchronous message delivery and processing.
NERV Audit: What Happened to the Data?
NERV Audit focuses on making historical changes to business entities easier to inspect and consume.
It builds on Hibernate Envers rather than replacing it.
The goal is to make revision history usable by applications, support tools, and administrative interfaces.
For example:
Account #1042
Revision 18
ACTIVE -> SUSPENDED
Revision 19
SUSPENDED -> ACTIVE
Revision 20
ACTIVE -> DEACTIVATED
The audit trail helps reconstruct committed entity changes.
The event infrastructure helps investigate the asynchronous workflow.
Both are useful, but they provide different evidence.
7. What Should a Production System Capture?
I find it useful to think about production diagnostics in terms of questions.
| Question | Useful source |
|---|---|
| Why did this request fail? | Logs, exceptions, traces |
| Was this event published? | Outbox and messaging records |
| Was the message processed? | Consumer and inbox records |
| What was the previous entity state? | Audit history |
| Who changed this entity? | Audit actor metadata |
| Why was the change requested? | Explicit business context |
| How was the system performing? | Metrics |
These mechanisms work best when their responsibilities are clear.
We don't need to audit every field or retain every log indefinitely.
We do need to identify which business changes matter and design reliable ways to investigate them.
8. The Engineering Principle
After years of working on enterprise Java systems, one lesson has become increasingly important to me:
A production system should make important business changes explainable through evidence, not guesswork.
That doesn't mean a single tool can explain everything.
It means having the right evidence available when an investigation begins.
Application logs help reconstruct execution.
Audit history helps reconstruct business-data changes.
Event-processing records help reconstruct asynchronous workflows.
Distributed tracing connects execution across services.
And explicit business context helps explain why particular decisions were made.
That's the philosophy behind NERV Audit and the broader NERV ecosystem.
Final Thoughts
Logs are essential for production support.
But they are not an audit trail.
A log statement may describe an operation that eventually rolls back.
A structured log message may not survive as long as the business needs its history.
And a sequence of application events may still be insufficient to reconstruct an entity's committed state transitions.
For Spring Boot applications using Hibernate, Envers provides a strong starting point for transactional revision history.
NERV Audit builds on that foundation to make historical changes easier to query, inspect, and consume.
Because when production support asks:
"Why is this account in this state?"
We should have better tools than searching through thousands of unrelated log messages.
Logs tell us what the application was doing. An audit trail tells us what happened to the data.
This is Article #2 in my NERV Audit series, exploring practical approaches to production-ready auditing in Java and Spring Boot.
Explore the open-source NERV projects: GitHub — CzetsuyaTech
Read the canonical article: CzetsuyaTech
Top comments (0)