Skip to content

Static Variables & Methods are Evil?

Sponsor: Using RabbitMQ or Azure Service Bus in your .NET systems? Well, you could just use their SDKs and roll your own serialization, routing, outbox, retries, and telemetry. I mean, seriously, how hard could it be?

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


You might have heard the recommendation to steer clear of static variables or methods. But is that really good advice? Let’s dive into why people say that, with a little nuance, and clarify the topic with some examples.

YouTube

Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.

Deterministic Behavior

public class AgeVerification
{
public bool Is18YearsOrOlder(DateTime birthdate)
{
var checkDate = birthdate.AddYears(18);
return checkDate.Date <= DateTime.UtcNow.Date;
}
}

First off, let’s talk about determinism. I have a method called is18YearsOrOlder, which takes a DateTime argument representing your birth date. The method checks if you’re 18 years or older by adding 18 years to your birth date and comparing it to today’s date. Sounds straightforward, right?

But here’s the catch: this method is not deterministic. Each time this method runs, it uses DateTime.UtcNow, which gives a different value every time. You’ll often hear people discussing pure functions, and what they mean by that is determinism. If I pass a specific value, I should always get the same result back. But in my case, that’s not happening, and my tests reflect that.

public class AgeVerificationTest
{
private readonly AgeVerification _obj = new();
[Fact]
public void NotOver18()
{
var result = _obj.Is18YearsOrOlder(new DateTime(2006, 10, 22));
result.ShouldBe(false);
}
[Fact]
public void Over18()
{
var result = _obj.Is18YearsOrOlder(new DateTime(2006, 10, 20));
result.ShouldBe(true);
}
[Fact]
public void NotOver18_2()
{
var result = _obj.Is18YearsOrOlder(DateTime.UtcNow.AddYears(-18).AddDays(1));
result.ShouldBe(false);
}
[Fact]
public void Over18_2()
{
var result = _obj.Is18YearsOrOlder(DateTime.UtcNow.AddYears(-18).AddDays(-1));
result.ShouldBe(true);
}
}

For instance, I had a test named NotOver18 where I passed in yesterday’s date. Yesterday it passed, but today it fails because today is exactly 18 years ago from yesterday.

To manage this, I created two more tests using DateTime.UtcNow so they would always pass.

So, is this a reason to label static methods or variables as bad? Not quite. It just shows that non-deterministic methods can complicate testing and reasoning.

Managing Non-Determinism

public class PlaceOrder
{
public Order Process()
{
return new Order(DateTime.UtcNow, 100);
}
}
view raw PlaceOrder.cs hosted with ❤ by GitHub

As another example: I have a PlaceOrder class with a Process method that returns a new order and includes a Processed property to indicate when the order was processed.

public class PlaceOrderTests
{
[Fact]
public void Test()
{
var obj = new PlaceOrder();
var result = obj.Process();
result.Processed.ShouldBe(DateTime.UtcNow, TimeSpan.FromSeconds(1));
}
}

In my tests, many assertion libraries will check the date and time, but I can’t know exactly what that date is. I might set a tolerance, saying it should be within the last second, but that’s still non-deterministic. In some contexts, that’s not a big deal, but in others, it can be problematic.

public class PlaceOrder
{
private readonly TimeProvider _timeProvider;
public PlaceOrder(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}
public Order Process()
{
var now = _timeProvider.GetUtcNow();
var total = now.DayOfWeek == DayOfWeek.Friday ? 50 : 100;
return new Order(now, total);
}
}
view raw PlaceOrder.cs hosted with ❤ by GitHub

For instance, if you place an order on a Friday and you get a 50% discount, how do I test that? The answer lies in making it deterministic. I modified the PlaceOrder method to accept a fake DateTime input. By injecting a time provider into the process method, I can set a specific date for testing, ensuring consistency.

public class FakeDate : TimeProvider
{
private readonly DateTimeOffset _utcNow;
public FakeDate(DateTimeOffset utcNow)
{
_utcNow = utcNow;
}
public override DateTimeOffset GetUtcNow()
{
return _utcNow;
}
}
public class PlaceOrderTests
{
[Fact]
public void HalfOffOnFriday()
{
var now = new DateTime(2024, 10, 18, 17, 13, 00);
var obj = new PlaceOrder(new FakeDate(now));
var result = obj.Process();
result.Processed.ShouldBe(now);
result.Total.ShouldBe(50);
}
[Fact]
public void FullPrice()
{
var now = new DateTime(2024, 10, 19, 17, 13, 00);
var obj = new PlaceOrder(new FakeDate(now));
var result = obj.Process();
result.Processed.ShouldBe(now);
result.Total.ShouldBe(100);
}
}

Coupling and Global State

Next, let’s discuss tight coupling with static methods. Using static methods limits flexibility because you can’t override their behavior. If a static method is non-deterministic, it can make your own methods non-deterministic, complicating testing and reasoning.

public class Customers
{
public Customer Get(int id)
{
return (Customer)Global.Cache.GetByKey($"Customer:{id}");
}
}
view raw Customers.cs hosted with ❤ by GitHub

Another common reason to avoid static variables is their global nature. Global variables can lead to uncertainty about their initialization and state. For example, I created a Global class with a static Cache property. If it’s null, has it been initialized? Do I need to connect to it first? These uncertainties are why global static variables can be problematic.

public static class Cache
{
public static List<Customer> Customers = new();
}
view raw Cache.cs hosted with ❤ by GitHub

Mutable static variables can also be an issue in multi-threaded environments. If I have a static cache containing customer data that’s not thread-safe, running a Parallel.For can lead to failures. In such cases, you need to ensure that your static variables are thread-safe. In .NET, there are thread-safe collections, such as ConcurrentBag available that can help mitigate these issues. If I change my List<Customer> to a ConcurrentBag<Customer> then we are now thread-safe. But you must be thinking about this in advance if you’re using static properties and how they will be accessed.

Utility of Static Variables

So, are static variables entirely terrible? Not really. Their utility often depends on context. For example, I changed my customer collection to a ConcurrentBag, which is thread-safe, and guess what? Problem solved! The tests passed without worrying about concurrency issues.

public class Distance
{
public static decimal MilesToKilometers(decimal miles)
{
return miles * 1.60934m;
}
}
public class TestDeterministic
{
[Fact]
public void Deterministic()
{
var result = Distance.MilesToKilometers(15);
result.ShouldBe(24.14010m);
}
}
view raw Distance.cs hosted with ❤ by GitHub

Let’s look at a static method like milesToKilometers. It takes in a number of miles and always returns the same value based on the parameters passed, making it deterministic. This illustrates that static methods can indeed be useful.

Static Variables & Methods

To wrap things up, the reasons people often suggest avoiding static variables and methods include:

  • Tight coupling and lack of flexibility
  • Global state uncertainty
  • Testing complications, especially with non-deterministic methods
  • Thread safety concerns in multi-threaded environments

But static variables aren’t the devil. They have their place, especially when used deterministically and with consideration for concurrency.

Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.