Working with a 5-6 years old system gives me a good chance to reflect on code written by other developers. When looking at an old code, even written by me, I often think “hmm what a mess! who wrote that code? what were they thinking?” That’s kind of things! The right attitude is that I should not have thought that. However, that is what’ve happened. To me, it is a good thing because I started to think of “I can do it better“.
Each codebase has its own story. This time I got a chance to look and troubleshoot another codebase. I decided to take a deeper step with questions to myself
What were the developers thinking? How would they end up in that design/decision?
The main point is for me to learn something. I am not interested in spending my time to criticise them.
The Story
Because of the contract and sensitive thoughts, I will rename and make up my terms. The system is a service that allows many systems call to log data. It has the same notion of any logging framework in .NET, such as log4net, NLog, … except this is a custom WCF service.
public class LogCommand { public string Product { get; set; } public string User { get; set; } public DateTime CreatedAt { get; set; } public string LogMessage { get; set; } } public class LogEntry { public Guid ProductId { get; set; } public Guid UserId { get; set; } public DateTime CreatedAt { get; set; } public string LogMessage { get; set; } } public class LogCommandHandler { public void Execute(LogCommand command) { // Begin a transaction because we use NHibernate to talk with SQL Server var productId = CreateOrGetProduct(command); var userId = CreateOrGetUser(command); var logEntry = new LogEntry { ProductId = productId, UserId = userId, CreatedAt = command.CreatedAt, LogMessage = command.LogMessage }; _session.Save(logEntry); } private Guid CreateOrGetProduct(LogCommand command) { // if this is a new product, create. // otherwise return existing product id return Guid.NewGuid(); } private Guid CreateOrGetUser(LogCommand command) { // if this is a new user, create. // otherwise return existing product id return Guid.NewGuid(); } }
There is more than above. But you get the idea. Here is the logic
- Get the right Product associated with the log. Insert a new Product record if a product is new.
- Get the right User associated with the log. Insert a new User record if a user is new.
- Insert a new LogEntry with the ProductId and UserId
And in the database, we have 3 tables
- Product: for products
- User: for users
- LogEntry: for log entries
The reason for having Product and User tables is to support searching from a WPF application (let’s call it LogViewer). At the LogViewer, users want to be able to choose Product from a dropdown. The same goes for users.
The system works as It is supposed to be …. until one day. When there are many concurrent calls to the service. It works unstable.
To cut the story short, the problem caused by the way it managed Product and User. When a production is created (but have not flushed to the database yet), it is put into a global cache to improve performance. When a next request comes in, the system will check the global cache first. Here is how the problem occurred
- Request 1: Create a new product, put in the global cache. But NOT commit transaction yet.
- Request 2: See that product in the global cache, it takes the product and assigns ProductId to the LogEntry. And it commits the transaction.
- The database foreign key constraint throws an exception because the ProductId has not persisted in the database yet.
Hmm? But Why?
The interesting part to me is the question I asked myself. Why? What brought the developer to that implementation?
Here is my theory.
Note: It is just me pondering my own thoughts as part of my learning practice.
Developers tend to focus on the technologies, on how to implement things. Many have been working with SQL Database for years. Thus, they have built a Relational Thinking Mindset. Let’s go back in time and try to imagine what they thought when they got the requirement.
We need to build a log service that allows us to store and search logs by product or user. A log entry will belong to a Product and be created by a User.
Oh cool! Got it. Then we will have 3 tables, one for Product, one for User, and one for Log. And we will have foreign keys from between Log and Product, between Log and User. Easy cake! Job done!
If I were him years ago, I would have done the same. What was missing? – The usage analysis.
Ok! Analysis
Move forward to now where I have years of developing, where I have a problem at hand, where I fight down the root cause. I meant I have so many information at my disposal. What questions would I ask? What concerns would I put?
What are the main operations?
- Write log
- Search log
- Get all products – for the search purpose
- Get all users – for the search purpose
What are the most frequently used operations?
- Write log
There is a good notion of CQRS in this service. The Write Log operation is Command, the rest are Query. Can we support queries without maintaining Product and User tables by ourselves in code? Yes, we can. SQL Server comes with the powerful concept: View.
By creating views for Product and User, the service can query on them. It gives the same result as maintaining tables.
Takeaway
What happens if we interrupt our default thinking process with
Hey! wait a minute!
That should be enough to allow us to think differently. It might help you start a questioning process.
When looking at a codebase, no matter how good/bad it is, try to reason the question
What were they thinking? How would they end up with the design?
I gain so much with this approach. So hope it works for you too.