In the ever-evolving landscape of software development, the power to create is at our fingertips. As developers, we bring digital dreams to life, crafting lines of code that breathe functionality into applications. So, we get carried away by the power of our lines of code and tend to forget that code is just a small piece of the universe of running an app. Looking at it from a different perspective, each line of code that we write carries a price tag and if we aren’t aware of it or if we don’t take full accountability of the financial repercussions of our code, we might find ourselves in awkward situations.
I was myself the type of developer that totally neglected the financial implications of the apps I was writing until I worked with a CTO that made me realize I was missing the big picture. It’s true, the value of the applications we write lies in the business problems we solve. However, all business problems have an associated cost. So, if the cost of solving the problems is higher than the associated cost of the problems themselves, does our application still bring value?
I won’t go to deep into these topics right now, but CTOs have a precise way of evaluating this aspect by looking at the ratio between Total Cost of Ownership (TCO) and Return of Investment (ROI). The ROI measures the financial returns generated by a technology investment relative to its costs. On the other hand, the TCO encompasses the entire cost associated with owning and operating a technology solution throughout its lifecycle. TCO includes not only the initial acquisition costs but also ongoing operational expenses, maintenance, and any other relevant costs.
CTOs must strike a balance between maximizing ROI and minimizing TCO. While maximizing ROI is a goal, it must be achieved without disproportionately increasing the overall TCO. Therefore, optimizing TCO contributes directly to improving ROI. By identifying cost-saving opportunities in the entire lifecycle of a technology solution, CTOs can enhance the overall return on investment.
So, where do our apps fit in? Well, our apps generate operational costs through the amount of compute power, memory, storage and networking they require. This operational cost is a fundamental part of the TCO that I described earlier. There are a few areas where we can easily optimize costs just by being aware of the problem.
The Database as the Root of All Evil
In most applications, the database is the root of all evil when it comes to costs. Inefficient database schemas, insufficient use of indexes and inefficient queries unnecessarily increase the operational cost of our applications. But here are a few things we can easily trim.
First of all, do we really need Unicode everywhere? This is a huge topic from my point of view since everything is NVARCHAR if we’re working with EF Core. But using NVARCHAR on columns that don’t need multi-lingual support (and therefore unicode) is a waste of storage resources.
Let’s imagine that we have a 999 GB database and all columns are NVARCHAR. If we’re running it on Azure SQL on the general purpose tier with locally redundant storage, this would generate a cost of around $137/month only for storage. Now let’s imagine that we change the data type of all columns to be VARCHAR instead of NVARCHAR. For the same setup we would now have to pay only $68.
Bottom line is that we can easily optimize database costs just by using the appropriate data types. And you can easily configure your column data types in EF Core and it would look something like this:
In this example we already see another optimization, which is setting the maximum length. By default, EF Core will use nvarchar(max) or varchar(max). This also makes inefficient use of storage. As developers we might be tempted to overlook this, but what I usually try to do is having a max length defined on each string property that gets written to the database.
- Inefficient queries don’t just hurt the overall performance of our applications, but they also translate into higher operational costs. These costs can be generated by:
- Excessive CPU use
- Excessive memory use
- Unnecessary network calls (for instance if we make several different DB calls for queries that can easily be retrieved in one)
- Here are some very useful tips that will help you shape your queries as efficiently as possible:
- Make proper use of indexes
- Always use projections to retrieve only data that you need
- Limit result size through efficient pagination
- Avoid cartesian explosion when loading related entities
- Favor streaming queries over buffering queries
- Always use
AsNoTracking()
for read-only use cases
Let’s Make Big-O Notation Great Again!
Big-O notation is a mathematical notation that describes the limiting behavior of a function when the argument tends towards a particular value or infinity. In computer science, it is primarily used to analyze the efficiency or time complexity of algorithms. I hated it and almost everybody that attended a “Data structures and algorithms” class does. It seems so abstract and useless.
Nevertheless, the efficiency of the algorithms we write directly affect the amount of CPU resources our algorithm will need. Scaling this to an entire application it could mean that we exhaust all the CPU resources on a server faster. So, we’ll autoscale it, that’s why cloud is cool, right? Right, but costly!
And if you think that time complexity is something we need to think about only if we do our own sorting algorithm or generally complex algorithms, think twice! A very common pattern that I see in a lot of code bases is the use of nested loops. Nested loops have a quadratic time complexity (O(n^2)), which is actually very bad. In probably around 80% of the cases I saw, nested loops could be avoided. Another example is recursion. Non-optimized recursions take an exponential time complexity (O(2^n)), which is also bad. Optimizing recursions might be very time consuming and complicated, so another way to go about it is to avoid it where possible.
The main point here is you should always take some time to understand what is the time complexity of the algorithms you write, even if they are very simple. I know, this is not something that comes naturally, but it will give you a better understanding of the software you write and ultimately it will make you a better software engineer.
Let’s Get that Memory Back!
Like CPU usage, memory consumption is also a very important aspect that might make our application break the bank! The reason is the same: if we run out of memory, we need mor memory. And this incurs costs. So, another thing we should take into consideration is to optimize memory consumption. In my opinion, allocations are a big pain point. When we write code, we tend to do a lot of allocations. This not only consumes memory, but it also takes time to de-allocate it. So, here are some very simple aspects that we can take into consideration to reduce memory allocation:
- Do we always need a class? A lot of times we could simply use structs instead of classes and this would reduce memory allocation.
- Passing by value might become a problem? Well, you can always pass and return by reference using the ref, in, ref readonly and out keywords. You can even have ref assignments!
- Directly manipulate memory through the Span<T> class. It provides safe access to blocks of memory
That’s mostly it. As a summary, to make sure that your application is not breaking the bank, make sure you ask yourself these 3 questions before shipping code:
- Does my code make efficient use of database resources?
- Does my code compute efficiently?
- Does my code allocate a lot of memory?
By doing this as an exercise on each PR you will make this way of thinking about software your second nature. And in the end, this will make the CTOs (for who you’ll write your code in the end) happier and they will always want to work with you.
P.S: If you enjoyed this article, you might also want to check my YouTube channel for a lot more and visual .NET content. I also run a newsletter that’s different to any other newsletter as it focuses more on things that will help you become a better engineer, not just a better coder. You might want to subscribe to it as well!