Tutorials / Build a REST API with C# and .NET · Lesson 6 of 15

On this page

    What Is State, and Why Is Sharing It Dangerous?

    The last lesson showed that a singleton is built once and shared by everyone. That was perfect for AppOptions. But is it perfect for everything? Before we meet the other lifetimes, we need to understand why the answer is no.

    This lesson is a short detour. It uses almost no Dependency Injection. It is about one idea that every lifetime choice depends on: state.

    Here is the path we will take:

    1. Things that never change, and why they are safe to share.
    2. Things that do change, and why sharing them goes wrong.
    3. A simple rule for choosing a lifetime.

    Take your time with this one. Once these ideas click, choosing a lifetime becomes easy.


    Start with something that never changes

    Remember AppOptions from the singleton lesson. It holds the database connection string and nothing else:

    public class AppOptions
    {
        public string ConnectionString { get; }
    
        public AppOptions()
        {
            ConnectionString = "Server=localhost;Database=BookShop;";
        }
    }
    

    ConnectionString is a member variable. That is a variable written inside a class, outside any method. Two things to be clear about:

    • It is not a global variable. It belongs to one object. Every AppOptions object carries its own copy.
    • Other classes can read it, but they have no way to change it. There is no setter, and nothing in the class modifies it after the constructor runs.
    A member variable lives inside an object OrderProcessor ReportGenerator AppOptions member variable ConnectionString lives inside this one object it is not a global variable can read cannot change The value is set once and stays the same.

    So from the moment AppOptions is built until the app stops, ConnectionString is the same value. Every class that uses it can rely on that. Nobody expects the database connection to change halfway through the day.

    Because it never changes, it is safe for everyone to share the same one. That is why it was a good fit for singleton in the last lesson.

    There is a second reason: it would be wasteful to build many copies of something that is always the same. Imagine a busy website that needs AppOptions a million times in one day. Building a million identical objects would use memory and time, and every one of them would hold the exact same connection string. One shared copy does the same job for everyone.


    A class with no member variables

    The same is true, even more simply, for a class that has no member variables at all. It just offers methods:

    public class PriceCalculator
    {
        public decimal AddTax(decimal price)
        {
            return price * 1.1m;
        }
    }
    

    There is nothing inside PriceCalculator to change. Give it a price and it gives you an answer. Give it the same price a hundred times and you get the same answer a hundred times. It is also an excellent candidate for singleton.

    If that is not obvious yet, here is why. Look at where the numbers live:

    • price is a parameter. It is handed in when AddTax is called, and it disappears when the method finishes. It is not stored in the object.
    • The answer, price * 1.1m, is worked out and handed straight back. The calculator keeps no copy of it.

    So the calculator never holds on to anything from one call to the next. Each call starts completely fresh, with only the values it was given. Whatever happened in the previous call has no effect on this one.

    Now picture two customers using the same single PriceCalculator at the same moment:

    • Customer A asks for the tax on 10.00 and gets 11.00.
    • Customer B asks for the tax on 50.00 and gets 55.00.

    Neither customer can disturb the other. A's 10.00 and B's 50.00 are handed in separately, worked on separately, and handed back separately. There is nothing in the middle for them to fight over. Remember this picture. We will see what changes when the thing in the middle does remember something.

    One calculator, nothing shared between calls Customer A Customer B PriceCalculator one copy only keeps nothing between calls 10.00 11.00 50.00 55.00

    Because the class remembers nothing, one copy is enough for every customer. This is the same saving as with AppOptions.


    Stateless and stateful

    We can now name this.

    • A class that cannot change after it is built is stateless. PriceCalculator has nothing inside. AppOptions has a value inside, but it is set once and never changes, so for our purposes it counts as stateless too.
    • State is what an object holds in memory that can change. In a class, that means its member variables.
    • A class whose member variables can change while the app runs is stateful.

    A picture to hold on to: a printed poster and a whiteboard, both hanging in the shop.

    • The poster shows the opening hours. It was printed once. Anyone can read it, a thousand people can read it at the same time, and it always says the same thing. Nobody can change it. That is stateless.
    • The whiteboard shows today's special offers. Anyone with a marker can wipe a line out and write something new. What you read depends on who wrote on it last. That is stateful.
    Stateless and stateful: poster and whiteboard Stateless a printed poster Open 9am to 5pm read it, never changes Stateful a whiteboard Today: 20% off ✎ anyone can wipe and rewrite PriceCalculator, AppOptions (left) · a shopping cart (right)

    Stateless things are safe to share, because nobody can change them. Stateful things are where the trouble starts.


    One shopping cart, two customers

    Now the opposite case. A shopping cart is a whiteboard: items get written on it, and what it shows depends on what has been added so far. Here it is in code:

    public class ShoppingCart
    {
        private readonly List<string> _items = new();
    
        public void Add(string item) => _items.Add(item);
    
        public void Show()
        {
            foreach (var item in _items)
            {
                Console.WriteLine(item);
            }
        }
    }
    

    _items is a member variable, and it changes every time Add runs. That makes ShoppingCart stateful.

    Now imagine the shop made it a singleton: one cart, shared by everyone. Customer A adds a mobile phone. Customer B adds lipstick:

    // The same cart instance is used for both customers
    cart.Add("Mobile phone");   // Customer A
    cart.Add("Lipstick");       // Customer B
    
    cart.Show();
    
    Mobile phone
    Lipstick
    

    Customer A's order and Customer B's order have been merged into one.

    Put yourself in Customer A's shoes. You came to buy a mobile phone. You press checkout, and your screen shows a mobile phone and lipstick. You never wanted lipstick. It belongs to Customer B, and you may even be charged for it.

    Two customers, one cart Customer A adds Mobile phone Customer B adds Lipstick ShoppingCart (singleton) [Mobile phone, Lipstick] One cart. Two orders, merged.

    Compare this with the PriceCalculator picture. There, the customers shared one object and nothing went wrong, because the object remembered nothing. Here the object remembers, so one customer's data leaks into the other's.

    When several users compete for the same object like this, programmers call it contention. Think of two people reaching for the same pen.

    Notice that here the customers took turns, and it was still wrong. The cart's state belonged to one customer, but a singleton hands that same state to everyone.

    On a real server it is even worse, because customers do not take turns. Their requests overlap, and the shared cart can break in stranger ways. You will meet that later in the tutorial. For now, the merged cart is already reason enough to be careful.


    The rule

    Put the whole lesson into one sentence:

    A stateful object should never be a singleton.

    A singleton hands the same instance, and so the same changing state, to every request. That is the merged cart.

    The class is... Lifetime
    Stateless (no member variables, or set once and never changed) Singleton is fine. Share it.
    Stateful (member variables that change) Never singleton. Give each customer their own copy.

    So what should a stateful class be instead? .NET has two more lifetimes for that. The first is transient: a brand-new instance every time something asks, so nothing is ever shared. That is the next lesson. The second, scoped, comes right after it.


    Recap

    • Member variable: a variable inside a class. It belongs to one object. It is not a global variable.
    • Stateless: nothing in the class can change after it is built (PriceCalculator, AppOptions). Safe to share, so a good fit for singleton.
    • Stateful: member variables that can change, like the items in a ShoppingCart. The object remembers things, and that memory can be altered.
    • Contention: several users competing for the same object, such as two customers merged into one cart.
    • The rule: a stateful object is never a singleton. It needs its own copy for each customer.

    Questions to think about

    Before the next lesson, try answering these in your own words. There are no answers given here on purpose. If you can explain them out loud to a friend, you are ready for the next lifetime.

    1. What is a member variable? How is it different from a global variable?
    2. TaxCalculator has one method, Calculate(decimal price), and no member variables. Is it stateless or stateful? Would you make it a singleton? Why?
    3. CurrentUser has a member variable Name, which is set when someone logs in. If it were a singleton, what would the next visitor to the website see?
    4. Name one class in a bookshop website that you would make a singleton, and one that you would not. Explain why for each.
    5. An Order holds the items one customer is buying. Singleton is wrong for it. What would you want instead: one copy for the whole shop, or one copy per customer?

    Next lesson: transient, the lifetime that builds a fresh copy every time, and a sneaky way the shared state can still hide even when you think you have avoided it.

    ← Service Lifetimes: SingletonService Lifetimes: Transient →