Monday, July 21, 2025

Fun with ScopedValues: An implementation of Implicit Observers

Executive Summary

This is a long and involved post about a new still-in-preview feature in Java called ScopedValues. It demonstrates a use for them that I think is very cool and fun, that is fairly magical, but without involving any compile-time trickery or bytecode manipulation. Nor even any reflection.

You will learn how to use ScopedValues to replace ThreadLocal patterns, and some reasons why it is better than using ThreadLocal.

I’ve intentionally glossed over some complicating details - thread safety in particular. The actual code makes use of non-blocking thread-safe tools, and some minor use of non-blocking thread-safe checks that are neither here nor there wrt the discussion of ScopedValues. For experienced developers, you’ll probably find some interest in thinking how that would work. For less experienced developers, try not to lose the overall thread in the weeds.

ScopedValue Introduction

Java’s Preview Feature, ScopedValue<T>, provides a utility similar to ThreadLocal<T>, which, if you’ve ever had the pleasure of using to spaghettifi a perfectly good codebase, may not seem like a very good thing at all. However, just as Land Value Taxes are similar to Property Taxes, and Universal Basic Income is similar to welfare, it’s the seemingly minor differences that transform these things into radically superior versions.

A ThreadLocal<T> userContext, more-or-less provides a hack that looks a lot like a Map<Thread,UserContext> userContextByThread. You can use it to store some object:

    // With ThreadLocal:
    ThreadLocal<UserContext> userContext = new ThreadLocal<>();
    
    // Set the value (and hope you remember to clean it later)
    userContext.set(new UserContext("alice"));
    
    // Access it from anywhere in the same thread
    UserContext user = userContext.get(); // "alice"
    
    // Oops, forget to clean up
    // userContext.remove(); // This often gets missed!

And, so long as no one has done any funny business with threading in your app between these calls, it’s all perfectly swell. Well, except for the magical values your code depends on that the compiler cannot check for. And, good luck with your unit and integration tests. Also, good luck tracking down why a value wasn’t in the ThreadLocal that you expected to be there.

So, how does ScopedValue give you access to this magic without screwing up your codebase? Using a scoped value looks like this:

    // Define once
    private static final ScopedValue<UserContext> USER_CONTEXT = ScopedValue.newInstance();

    // Use in a controlled scope
    ScopedValue.where(USER_CONTEXT, new UserContext("alice")) 
       .run(() -> {
          // Only here can we access the value
          UserContext user = USER_CONTEXT.get(); // "alice"
          processUserRequest(user);
      
          // No need to clean up - happens automatically!
       });

Here we’re putting the value in the ScopedValue. And then within that scope, we run some code - a Runnable. Could be a ScopedValue.CallableOp<T,EX> which returns a value:

    R processResult = ScopedValue.where(USER_CONTEXT,new UserContext("alice"))
        .call(ScopedValue.CallableOp<R,EX> myCallable);

This will add UserContext("alice") to the “scope”. It can be retrieved like this:

    USER_CONTEXT.get();
    USER_CONTEXT.orElse(defaultSpecialObject);

It’s a lot like an Optional in how you can retrieve or test for existence of a scoped value. These values are put and retrieved per thread, so multiple threads can all call ScopeValue.where(USER_CONTEXT,…) and put different values into the scope that will be tracked for that thread.

Why is this better than ThreadLocal?

While the code is being run within this ScopedValue, if it uses StructuredTaskScope to run child threads, those threads will inherit the Scoped values. There is no such inheritance with ThreadLocal - if you spin up new child threads, you will have to copy values manually to it.

If code runs ScopedValue.where(USER_CONTEXT,differentUser) while running within that scope, it will not erase the previous value held, but instead stores it in a stack, so that when this inner scope is done, the value of the Scope returns to what it was previously. When using ThreadLocal, you’d have to implement such Stack logic yourself.

When a scope ends, the value reverts to null and is not available. When using ThreadLocal, old values often get left lying around, which is no bueno.

ThreadLocal is also memory intensive if you’re using hundreds of thousands or millions of Virtual Threads, as it stores those values with each thread. ScopedValue does things a bit more efficiently.

ThreadLocal<T>                         | ScopedValue<T>
---------------------------------------|----------------------------------------
Values persist indefinitely            | Values automatically cleaned up when scope ends
No inheritance to child threads        | Values inherit to child threads (with StructuredTaskScope)
Manual stack management needed         | Built-in stack management for nested scopes
Prone to information leaks             | Automatic cleanup prevents informational leaks
Doesn't play well with virtual threads | More efficient than ThreadLocal when using virtual threads

Now the fun

Implement an implicit observer system using ScopedValue.

An implicit observer system lets code automatically react to data changes without explicit registration (or deregistration) of listeners. When a piece of code accesses a value, it is automatically registered as interested in future changes to that value.

I’ll introduce you to the magic we can have with such a system before describing how it works, so bear with me.
With an implicit observer system, we can write code like this:

    public class ComplexGUIWidget {
        private UserState currentUser;
        private Label nameLabel;
    
        public ComplexGUIWidget(UserState currentUser) {
            this.currentUser = currentUser; 
            init(); // <- we create the layout for this component, add the nameLabel to the gui...
            Reactor.begin(this::redraw);
        }
        
        
        public void redraw() {
            nameLabel.setText(currentUser.get().getName());
        }
    }

    // Somewhere else in the codebase
    currentUser.set("Bob");  // <- triggers ComplexWidget redraw() to be called again!

And now the name label will update it’s value anytime any code anywhere changes the userState:currentUser value. We might have some kind of SystemState class that includes the currentUser field, and we pass it around or dependency inject the SystemState to our various classes. It’s handy because an application can focus on managing the state of data (by talking to the SystemState class), and any code anywhere can build itself to automatically react to it. The secret sauce is in the Reactor.begin() method, of course. It is all enabled by the ScopedValue functionality.

    public class Observing {
       // The ScopedValue that tracks the CURRENT_OBSERVER
       private static final ScopeValue<Observer> CURRENT_OBSERVER = ScopedValue.newInstance();
        
                 
        public static void runWithObserver(Observer observer, Runnable operation) {
            ScopedValue.where(CURRENT_OBSERVER, observer).run(operation);
        }
      
        public static Observer getCurrentObserver() {
            return CURRENT_OBSERVER.orElse(null);
        }
    }
     
    public interface Observer {
 
        void notifyChange();
     
        default void observe(Runnable runnable) {
            Observing.runWithObserver(this, runnable);
        }
    }

    public class Reactor implements Observer {
        private Runnable actor;
        private boolean dirty = true;
    
        public Reactor(Runnable a) {
            this.actor = a;
        }

        public void notifyChange() {
            if(!dirty) { //we don't act on every notification, only if we're not dirty.
                dirty = true;
                act();
            }
        }
        
        private void act() {
            // using default observe method inherited from Observer interface
            observe(actor);
            dirty = false; // we ran, we're now clean
        }

        public static Reactor begin(Runnable action) {
            Reactor actor = new Reactor(action);
            Thread.ofVirtual().start(() -> {
                actor.act();
            });
            return actor;
        }
    }

By wrapping the Runnable actor in the call to “observe()”, we are adding ourselves as the CURRENT_OBSERVER to the ScopedValue. We will show the code that reads this ScopedValue in order to track access by Observers.

I didn’t show the thread-safe implementation of the notifyChange or act, largely because it is more complex by far if done thread safe. There would be multiple ways to do this, including just slamming “synchronized” keywords down.

On the other side, we need our containers that actually call our Observers notifyChange methods. These can be pre-built boxes that hold a single value:

    public class ValueDataBox<T> implements DataBox<T> {
    
        // here is the value we contain
        private final AtomicReference<T> value = new AtomicReference<>();
        // we track who called us so we can notify them of a change
        private final ConcurrentLinkedQueue<Observer> dependents = new ConcurrentLinkedQueue<>();

        public T get() {
            addDependent();
            return value.get();
        }
    
        private void addDependent() {
            // refer to our interfaces above, THIS is where we get the CURRENT_OBSERVER from the ScopedValue
            Observer dep = Observing.getCurrentObserver();
            if (dep != null) {
                dependents.add(dep);
            }
        }
     
        public void set(T newValue) {
            final T oldValue = value.getAndSet(newValue);
            if (!Objects.equals(oldValue, newValue)) {
                // if the value actually changes, let folks know
                notifyDependents();
            }
        }
    
        private void notifyDependents() {
            dependents.foreach(Observer::notifyChange);
            dependents.clear();
            // non-thread-safe implementation for simplicity here
            // also important, the list of dependents is cleared when we do this, and so observers are automatically unregistered as listeners.
            // to become listeners again, they have to request our data, which they will do upon notification if they are still interested.
        }
    }

Walk Through of the Flow of Data

The Reactor at the bottom is what triggers the recalculation of all the values in the boxes. Without it, everything just marks itself as “dirty” and then sits. So, a reactor is basically a Runnable function that does some side effect usually, such as drawing to a GUI.

Reactor function -> adds itself as the current Observer in the ScopedValue -> calls for values from boxes -> Boxes ask who is the current Observer from the ScopedValue -> add observer as dependent -> return current value -> Reactor uses values to draw something

When a box’s value changes:

iterate through dependents -> call `notifyChange` for each -> clear list of dependents

Note that ALL of these various data boxes, observers and reactors you create are serviced by a single ScopedValue -

    class Observing {
        private static final ScopeValue<Observer> CURRENT_OBSERVER = ScopedValue.newInstance();
    }

it’s not like there are many being created to do all this tracking. Furthermore, as a user of this library, you don’t even need to know it exists!

Lazy Calculated Values

We can also have have purely calculated values, where we have a DataBox implementation, but also we are an Observer, and our get() method returns a cached value until we receive a notifyChange call, and we ditch our cached value. We don’t eagerly recalculate, but only when our get() method is called and we’re “dirty”, so to speak. In this way, the system is a push-pull system. notifications are sent (pushed), but new values are only calculated on demand (pull, or lazy eval).

Wrapped Containers

We can also do more complex things, such as tracking dependents and sending notifications for values that we aren’t directly containing. We can wrap a List, for example, and have multiple Trackers that we manage somewhat manually upon requests for list data. Ie, when list.iterator() is called, we have a tracker wholeListTracker and:

    public Iterator<T> iterator() {
        wholeListTracker.trackAccess();
        return wrappedList.iterator();
    }

    public boolean add(T newItem) {
        wrappedList.add(newItem);
        wholeListTracker.notifyChange();
    }

And now our wholeListTracker will track observers and send notifications to whomever. Truly wrapping a List and enabling users of the list to depend on notification of any changes, no matter how they happen is involved, but once implemented, is simply usable as a regular list that magically notifies you when it’s changed. If you have a GUI method:

    public void drawSomeGUIStuff() {
        clearAwayCurrentButtons();
        // myMagicNameList is a wrapped List as described above
        for(String name : myMagicNameList) {
            addButtonForName(name);
        }
    }

    // somewhere, maybe in our constructor or init() method:
    Reactor.begin(() -> drawSomeGUIStuff); // <- this starts the process

So, whenever the list changes, we clear the current buttons and redraw them. No other code has to be coupled to this code to make it happen. No other code has to “remember” to notify us, or even know this gui component exists. If the gui component is removed, it will close itself and via some code around the concept of “closing” these observers that I’ve not shown, our draw method is never run again and our component is removed as an observer and we have no listener memory leak.

Difference between Events and State Changes

It should be noted that this is not an event system (I do have an implicit dependency event stream implementation though). The difference between events and state changes is subtle, but crucial. If you have a List set up as a notifying box as above, and then someone adds 3 new items to the list, your reactor functions may get 1, 2, or 3 notifications of changes. The only guarantee is that the last notification received will be for the final and current value of the List.

This is an advantage, if used properly in a system where you are building elements that need to reflect current state, as opposed to in a system where you need to perform actions based on events. In the latter, you need a different mechanism. The advantage of the state reflection system is there is some built-in back-pressure handling, as repeated calls to notifyChange are essentially debounced.

Another advantage is we rarely get to think about our application logic purely in terms of state. We are typically using event-based systems to mimic state based changes, which generally proves awkward and verbose. Also tend to be error prone especially as a system grows and you continually have to fit new behaviors into the current scattered state using various events, and your event functions get more and more complex as they try to keep a growing list of objects correctly in sync. Whereas in a proper state system, you can keep your state together and gather the logic around them, and then build reactors anywhere they need to be to reflect the state. So long as your reactors don’t go changing state, it works very well.

Final Thoughts

This sort of implicit observer setup is not a new idea - you can find something similar in javascript’s MobX library, and the concepts of Functional Reactive Programming go back at least to the 90s. I first encountered a similar framework working at Xerox on embedded printer GUIs. In that case, we didn’t have ScopedValues, and so when the “cells” called get() on each other, they actually sent themselves in the call so the provider cell could add the dependent cell as a dependent. So, the calls were SomeValue x = dataBoxWithValue.get(this). This had the unfortunate effect that code that worked in this fashion had to know that it was working as a Cell.

With the ScopedValue, it can be made implicit, and we can wrap otherwise straightforward methods using the Reactor.begin(Runnable) method. It also allows us to manually track read and write access to any property we want, alongside the property, and thus convert external classes to be DataBoxes.

When you build a GUI with this system, you need to shift your thinking about how you organize knowledge of state and interactions. Rather than building models and views and controllers, with controllers sort of wiring everything up between the models and views and views adding themselves as listeners to models, you set up graph structures of state. At the bottom are very simple single-value boxes where simple facts are stored (user is X, user has selected mode Y, mouse is over widget Z). Next are calculated values that look at those simple values, and maybe multiple of them, and calculate some intermediate value. I have used this sort of thing to build up complex Permissions structures, where at bottom is a basic file-level permssion information, but then built on that are more temporary and contextual changes to permissions that can happen due to perhaps multiple users looking at an object, or entering a “read-only” mode for some purpose, or maybe entering read-only mode because other processes are currently running and you want to prevent changes, etc. Your graph of state can be as capable and complex as you like.

And then your GUI representation of state is built in a way where you’re just writing functions that examine current state and draw themselves accordingly, and they will be re-called when any of the state they looked at changes. If you have hundreds of thousands of such elements, you can set up your state and functions such that these notifications go out very selectively, and only the parts of your gui that need to change run their updates.

ScopedValues of course can have many uses, I just think this one is pretty neat.

No comments:

Post a Comment