Architecture: local state on the server and global state on the client?


#1

Hello!

I’m painting some broad strokes here. Please bear with me!

As I’m learning about component based architecture (Integrant, Duct, Component, Mount), I’m seeing a contrast to centralized architecture (re-frame). Local state inside the components on the server, and global state on the client.

Claim/hypothesis: We use global state on the client because it’s really easy to manage and update content. For the server, the model breaks down because we have too many surfaces of interaction.

Why don’t we use something like re-frame on the server?

Prove me wrong? Do I have a point? Have a reference to share? Are there really similarities between Integrant and re-frame? Please comment! :sunglasses:

Teodor


Injure - v1
#2

Frontend and Backend are not equal. One Backend typically serves many users concurrently or even parallel. A Frontend is rarely used by multiple users at once. Frontend also typically deals with way less data than the Backend.

re-frame also typically only deals with one “global” database, which you can compare to your backend SQL database if you like. There are many other components in the backend that you don’t typically see on the Frontend either (eg. HTTP servers).

You may also want to run multiple instances of the same component on the backend (eg. when dealing with 2 different databases) and that is something where re-frame completely breaks. You can see this in action in re-frame-10x where it can’t just spin up a secondary instance and instead has to bundle its own re-namespaced version of re-frame as it would otherwise clash with your app database.

IMHO you should avoid global state whenever and wherever you can. You can get away with it until you can’t. Frontend typically runs in its own isolated world where you can get much further until you do things like re-frame-10x where it becomes apparent why you maybe shouldn’t do it.


#3

Keechma is an example of a client side reagent based library without global state. I’ve used it extensively and find it much simpler to extend than re-frame.

I think that “no global state” is a good goal to have whether server side or client side.


#4

Forget global state, try to think in terms of scope, lifetime and coupling.

First, global scope. This means that everything can access the Var, from anywhere. It also means the Var needs a place and a name, which are defined statically.

Now the lifetime can be thread bound, with dynamic Vars, or app bound otherwise.

Finally, coupling is that the place for the global state is hard-coded, and all things that depend on the state must know where to get it.

So if we have global scope, app lifetime, and name and place coupling, what are the pros/cons.

Pros:

  • straightforward
  • centralized

Cons:

  • Not thread safe
  • Singleton
  • Place coupling (bad for reuse, and likely to cause breakage)

You can solve thread safety with proper coordination, like what Atoms provide.

Singleton is harder to solve. I can’t even think of a way to do it.

Place coupling if you care about reuse and change over time is probably the biggest issue. Your components are no longer independent. They all depend on the state existing in the place they expect with the name they expect, with the value being of the shape they expect. Inversion of Control is often the alternative to this.

Now, straightforwardness and centralization are good pros. And I think they are the reason why front-end frameworks sometimes chooses this approach even though it has the listed cons. It’s all about trade offs and alternatives.

I think for backend, thread safety, place oriented and Singleton are bigger issues. And that’s why this is often avoided in those context.


#5

Everyone, thanks for the insightful replies! I’m really glad I asked. I’ve needed a little time to digest. I realize that I have a less mature understanding of backend architecture than I have of frontend architecture.

@thheller, Hello! Really enjoyed your The REPL appearance. I believe you mentioned web components, which caused me to do some useful research. Comparing re-frame to a databse instead of an architectural pattern really makes sense. If you’re connecting to two different databses, and it’s not out of necessity, that sounds like a bad idea.

@mjmeintjes, thanks for the link. I haven’t seem Keechma before. Do you think choosing betwen Keechma and re-frame is primarily preference? Or is it possible to say where one works well and the other breaks down?

@didibus, what you’re saying makes sense to me. If you care for it, I’ve got a follow up.

Finally, coupling is that the place for the global state is hard-coded, and all things that depend on the state must know where to get it.

  • Place coupling (bad for reuse, and likely to cause breakage)

Is the error that you couple the lifetime of some running system to a single instance, running in a single place? Is that “place-oriented programming”?

Also, if you’ve got a reference you recommend on inversion of control, I’d gladly take one. I’m starting to understand the value of dependency injection, and have used it to great effect on my team. But IoC eludes me.

Thanks, everyone!


#6

The single instance thing is my other con, the one I called singleton.

The place oriented issue is where your state is located and how to find it. To be very concrete, it’s literally that your functions and their code have the reference to the state hard-coded in them.

(def state (atom {:money 0}))

(defn add-money [amount]
  (swap! state update-in [:money] #(+ amount %)))

Now, add-money cannot be tested on its own. You need to setup the global state var for it. You can’t reuse add-money in other contexts, because it is coupled to the one where there exist a global state var. And if you ever want to change the place of the global, you break a lot of code, every function that was referencing it directly in a hard-coded way will be broken. Changing its place can be like renaming it, or moving it to a different namespace, putting it behind a database, etc. Literally the place where you store it, store the data, the state in this case. Your state is now stored in a global. If you moved it to a MySQL table, the above code would be broken, because it was depending on the exact place where the state used to be stored, in this case a global var.

Now IoC (inversion of control) would dictate that what is normally in the control of the callee be inverted, so that it is now in control by the caller. One common way to do that is DI (dependency injection).

In my example, finding the state is under the control of add-money. It does so using a hard coded reference to a global var. We want to invert this, so that the caller, the function that will call add-money is instead in control of where to find the state.

Here we will do it using DI, we will inject the state map into the add-money fn to invert the control.

(defn add-money [state-map amount]
  (update-in state-map [:money] #(+ amount %)))

That simple! We inverted the control of finding the state map from the callee to the caller, by injecting the state map into the callee.

Now, there’s more things you can invert. For example, add-money is still in control of extracting money from the state map, and updating it with the new value. You could invert that as well, still using DI:

(defn add-money [money amount]
  (+ amount money))

There’s not a whole lot of other ways to do IoC, other than with DI. But there are still some. People say IoC is the principle, and DI is a mechanism to implement the principle.

Here’s another way to apply IoC. In this case, we’re going to use hooks, or what is often called plugins.

(declare ^:dynamic get-state-fn)
(defn add-money [amount]
  (update-in (get-state-fn) [:money] #(+ amount %)))

What is happening here is that add-money gives the caller a hook for it to plugin the “finding the state map” logic into. Thus the caller is now in control, but it doesn’t inject dependencies to do so, instead, it just implements the get-state-fn hook to create a plugin that add-money knows how to use. Like so:

(binding [get-state-fn (constantly {:money 10})]
  (add-money 300))

I hope this makes things a lot clearer for you.

If you want to read more about it, I’ll shamelessly recommend an old blog post of mine: https://www.rubberducking.com/2016/06/inversion-of-control-ioc-vs-dependency.html which also includes links to even more reading.


#7

Excellent writeup :slight_smile:

Thanks!