How Eliminating Database-Baked Logging Led to Lighter Code, Easier Testing, and a Reusable Component
For too long, I wrestled with a common architectural headache: logging baked directly into stored procedures. It’s a pattern many of us fall into, seemingly convenient at first glance. But, as my application grew, this convenience quickly turned into a quagmire of redundant code, difficult testing, and a complete lack of separation of concerns.
The problem was obvious, yet insidious. Every stored procedure that needed to log an event – whether a success, a failure, or just a key operation – had its own setup for logging and redundant calls to the same two logging procedures within the database. This wasn’t just messy; it was a fundamental violation of the Single Responsibility Principle. My stored procedures were no longer just about data manipulation; they were also responsible for logging, bloating them with noise and making them harder to understand and maintain.
The Pain Points of Database-Baked Logging
Let’s break down the major issues I faced:
- No Separation of Concerns: Stored procedures should ideally focus solely on database operations. Introducing logging tangled their responsibilities, making them less focused and harder to reason about.
- Testing Nightmares: Unit testing database logic becomes incredibly cumbersome when logging is intertwined. You’re not just testing the data operation; you’re also inadvertently testing the logging mechanism, which complicates setup and verification.
- Code Bloat and Redundancy: Imagine dozens, if not hundreds, of stored procedures, each with similar lines of code dedicated to logging. This creates massive duplication, making global changes or bug fixes a nightmare. A simple change to how logging should work required touching countless stored procedures.
- Reduced Readability: The core logic of the stored procedure was often obscured by the logging boilerplate. It was like trying to read a book where every other paragraph was a footnote.
The Solution: Centralized Logging with ExecuteWithLogging<T>
My journey to a cleaner architecture led me to a powerful realization: logging belongs higher up the stack, specifically within the application layer. This is where business operations are orchestrated, and it’s the perfect place to wrap these operations with cross-cutting concerns like logging.
The core of my solution was implementing a generic method, ExecuteWithLogging<T>. This method acts as a wrapper around any operation that returns a value of type T.
Here’s the high-level concept:
C#
public async Task<T> ExecuteWithLogging<T>(Func<Task<T>> operation, string operationName)
{
// Log "starting operationName"
try
{
T result = await operation();
// Log "operationName succeeded"
return result;
}
catch (Exception ex)
{
// Log "operationName failed" with exception details
throw; // Re-throw the exception after logging
}
}
Now, instead of logging within a stored procedure or directly in every service method, my application layer calls look something like this:
C#
// Before (conceptual)
// var data = await _dbContext.GetSomeDataFromSPWithLoggingAsync();
// After (conceptual)
var data = await _logger.ExecuteWithLogging(
async () => await _dbContext.GetSomeDataAsync(),
"GetSomeDataOperation"
);
The Unlocked Benefits
The impact of this refactoring was immediate and profound:
- Lighter, Cleaner Database Code: All logging pollution was removed from my stored procedures. They are now lean, focused, and dedicated solely to database operations. This makes them significantly easier to understand, optimize, and maintain.
- Simplified Testing: Testing the database layer became a breeze. I no longer needed to worry about mocking or asserting logging calls when testing my stored procedures. My unit tests for the application layer can now easily mock the
ExecuteWithLoggingmethod or verify its calls, separating the concerns perfectly. - Consistent Logging: Every operation wrapped by
ExecuteWithLogging<T>now logs in a consistent, standardized way. This uniformity is invaluable for debugging, monitoring, and auditing. - A Standalone Logger! Perhaps the most exciting byproduct of this effort is the creation of a truly decoupled, standalone logging component. Because
ExecuteWithLogging<T>is generic and independent of specific business logic, I can now package it as a reusable library. My next step is to componentize this logger and potentially offer it to other developers via GitHub and NuGet.
This refactoring was a huge effort, involving significant changes across the application and database layers. But the return on investment in terms of code quality, maintainability, testability, and future reusability has been immense. If you’re struggling with tangled logging in your data access layer, I highly recommend exploring a similar approach to centralize and decouple it. Your future self (and your teammates) will thank you.
About Me
Hey — I’m Paul, a software developer building legal tech products with .NET and a lot of stubborn curiosity. I’m passionate about turning complex business logic into clean, testable code, and I write about the ups, downs, and discoveries along the way.
Want to follow along? I’m @PaulAJonesJr on Twitter, and I blog at PaulJonesSoftware.com. You can also reach out directly: paul@pauljonessoftware.com.


Leave a comment