console.blog( ,

Fluxbus - an application architecture pattern

Facebook the Behemoth

Facebook is pretty good at a couple of very interesting things.
It also has two and a quarter billion monthly active users. The world has around 7.6 billion people in it, which means something like 30% of the world uses Facebook at least semi-regularly.

Let's stipulate - for a moment - that you are not Facebook.
Let's stipulate - just for now - that you do not have 2.25 billion users. Or even 1 billion. Or even 1 million. If you have 1 million users: congratulations! You are 4.44e-4 the size of Facebook, or .000444 the size - 4.44 hundred-thousandths the size.

The red block is you, with 1,000,000 active users!

Given that stipulation, can we agree that you do not have the same problems Facebook has? You have the same problems that Facebook has in the same way that - say - your household has the same budget problems that the U.S. Government has.
I have yet to write at any length about the negatives (and it is all negatives) of cargo-cultism (other than another one-off comment), but suffice to say: if you think anything a company like Facebook is doing solves the same problems you are having, you are woefully deluded - by your own doing.

Now, this doesn't mean that technological accomplishments that a company like Facebook is doing can't be applied to everyone else (and we are really everyone else; Facebook is literally in a league of its own in terms of userbase).
It's just that the things Facebook is doing need to be tweaked.
Take - for example - the Flux architecture (and specifically it's outspringing: Redux). These things seem to look pretty good, but once you really start using them heavily, you begin to make all kinds of sacrifices because they prescribe an application structure.
For some, this may be fine. Indeed, I don't think many people will disagree that a pattern like Redux is prescriptive. "Yes. That's kind of the point," they will say. Unfortunately, I have found that this level of prescriptivism doesn't work well for me.

Why Redux (and Flux, sort of) Doesn't Work For Me

  1. Asynchronous is really complicated

    Have you ever used Redux? If you have, then you probably encountered a huge problem almost immediately: You can't just load data any more.
    Since everything has to result in a synchronous change to the store that subscribers then consume, your actions have to be thunks, and you probably need to use a complex middleware to interrupt the normal action processing pipeline until your async action finishes.

    This isn't strictly necessary with either generic Flux architectures or with Redux specifically, but if you implement asynchronous behaviors outside the "Redux way", well, you're not doing it the "Redux way".
    All sorts of things start to get more complicated. And - after all - isn't the point to standardize the way your data flows in one direction?

    Because Flux is much more general, I think asynchrony fits much better into that pattern, but it's still not easy: there's a whole example project just for async.

  2. Every app I've ever worked on uses more than one type of storage

    In most of my applications, there's a heirarchy of data. The absolute source of truth for 1 or two bits of information is a cookie (this cookie contains an authentication token, maybe one or two other things).
    We need a cookie because we need login to work across multiple subdomains, and solutions that don't use cookies are a ridiculous re-invention of the top level domain cookie that's worked forever.
    When the application(s) start, they check this cookie (what if the user logged in as a different user between the previous load and now?) and load the correct information from it into localStorage, sometimes triggering API requests to get more complete information and store that information in localStorage.

    During runtime, the application might change certain values in localStorage that need to persist to the next load, but don't need to go back to the server.
    Even if the application doesn't do that, it absolutely will store a large amount of data in one (of many possible) indexedDB databases.
    This data is indexed, timestamped, and keyed by request URL (if you request a?b=1, and we have that data, you get a free resolution, no network needed!).

    Finally, also during runtime, the application stores some information in the "window" storage also known as the global object in a custom namespace. This is just a scratch space for one-off memory, but it's very convenient for storing something complex like a function. Like a router.

    Flux (and Redux) give you exactly one kind of storage - the latter, a runtime-level storage. You can freeze it to a cold state and hydrate it again on app load, but this feels immensely wasteful.
    Why is my data not just there to begin with? That's the place it lives, why am I storing it in the Flux store as a dumb middleman when I could just be using the actual storage directly?

  3. Every single listener is called for every single change

    This one is a shared problem in both Redux and Flux.
    It's particularly bad in Redux, which demands that you use a single store for everything, but it's just scaled down to each Flux store.

    Since the subscription pattern in Redux is store.subscribe( someCallback ), there is no way to filter how many functions are run when the state updates.
    Imagine a moderately complex application that has a state value like { messages: { loading: true } } and a few hundred subscribers.
    How many of those subscribers care that the messages stop loading at some point? Probably 1. Wherever your chat module is, it's probably the only place that cares when the messages stop loading and it can hide a spinner.
    But in this subscription paradigm, all of your few hundred subscribers are called, and the logic is checked to determine if they need to update. The inefficiency is mind-boggling.

    Again, Flux doesn't quite have the exact same problem: not only because of the fact that you can have multiple stores for specific intents, but also because it doesn't prescribe any particular subscription pattern.
    You could implement a pre-call filter that allows subscribers to only be called for particular actions that they care about, but this is so far off the beaten path it's never been seen by human eyes before.

    The alternative is to only pull state during rendering but if you do that - oh my - what is the point of a centrally managed store? Isn't the value that your components are always up to date with the same state changes?
    Sure, you could have a single subscriber at some very high level that's making the decision to pass new data to its children components, but then why does this component know about the store at all?

The Part of Flux That Works For Me

The strict actions.

Dear sweet ${user.favoriteDeity}, standardized, universal, consistent actions are the best.

Every possible application workflow either starts with, ends with, or both starts and ends with a known action.

I honestly don't know why this pattern isn't used more often. When every part of an application that works on a "global" scale (think: routing, changing views, loading data) is codified and standardized, all you have to do to figure out how something works is follow the trail of actions it triggers.

You might have noticed above that there are some actions that don't seem to correspond to something going into state. You're right, this architecture isn't about resolving everything to state, it's about connecting the disparate parts of your application together without complex inter-dependencies.

This is the part of Redux/Flux that they get absolutely, gloriously correct: one, single, way for the application to encapsulate behavior.

The Best of Flux: Fluxbus

As mentioned above, the part of Redux/Flux that is good is the standardized messaging. The parts that are bad are myriad, but not the least are overly cumbersome data flow, restrictive storage, and the shotgun approach to calling subscribers.

The problems boil down to the fact that the Flux architecture is prescribing too much. If the prescription was simply "All application changes are made by publishing a standard action" then it wouldn't have any of the problems.

So, let's do just that. Fluxbus requires that all application changes are made by publishing a standard action through a central message bus. There's nothing more to it than that.

Example Implementation

// We're using rx to avoid having to implement our own observables
import { Subject } from "rxjs"; 

var bus = new Subject();
var LOGIN_REQUEST = {
	"name": "LOGIN_REQUEST",
	"username": "",
	"password": ""
};
var LOGIN_SUCCESS = {
	"name": "LOGIN_SUCCESS"
};
var LOGIN_FAILURE = {
	"name": "LOGIN_FAILURE",
	"problems": []
};

function publish( message ){
	return bus.next( message );
}

function subscribe( map ){
	// Returns an unsubscriber
	return bus.subscribe( ( message ) => {
		if( map[ message.name ] ){
			map[ message.name ]( message );
		}
	} );
}

subscribe( {
	"LOGIN_REQUEST": ( message ) => {
		fetch( "https://example.com/api/login", {
			"method": "POST",
			"body": JSON.stringify( {
				"username": message.username,
				"password": message.password
			} )
		} )
		.then( ( response ) => response.json() } )
		.then( ( json ) => {
			if( json.problems.length == 0 ){
				publish( LOGIN_SUCCESS );
			}
			else{
				publish( Object.assign(
					{},
					LOGIN_FAILURE,
					{ "problems": json.problems }
				) );
			}
		} );
	}
} );

What's going on here?

// We're using rx to avoid having to implement our own observables
import { Subject } from "rxjs";

var bus = new Subject();
var LOGIN_REQUEST = {
	"name": "LOGIN_REQUEST",
	"username": "",
	"password": ""
};
var LOGIN_SUCCESS = {
	"name": "LOGIN_SUCCESS"
};
var LOGIN_FAILURE = {
	"name": "LOGIN_FAILURE",
	"problems": []
};

This is just setting up an RxJS Subject and three actions. Note that this is what a bus is. That's it. You can publish messages into it, and you can subscribe to published messages.

function publish( message ){
	return bus.next( message );
}

function subscribe( map ){
	// Returns an unsubscriber
	return bus.subscribe( ( message ) => {
		if( map[ message.name ] ){
			map[ message.name ]( message );
		}
	} );
}

This is just wrapping the native Rx methods with a simple helper (in the case of publish) or our filter magic (in the case of subscribe). The subscriber takes a map of functions keyed on a message name. If that message name is seen, that subscriber is triggered.

subscribe( {
	"LOGIN_REQUEST": ( message ) => {
		fetch( "https://example.com/api/login", {
			"method": "POST",
			"body": JSON.stringify( {
				"username": message.username,
				"password": message.password
			} )
		} )
		.then( ( response ) => response.json() } )
		.then( ( json ) => {
			if( json.problems.length == 0 ){
				publish( LOGIN_SUCCESS );
			}
			else{
				publish( Object.assign(
					{},
					LOGIN_FAILURE,
					{ "problems": json.problems }
				);
			}
		} );
	}
} );

This sets up a subscription that waits for a login request. When it sees one, it tries to log in with the provided username and password. If that login succeeds, this publishes a LOGIN_SUCCESS message. If the login fails, it publishes a LOGIN_FAILURE message.

Of course, you might want to organize this a bit better.
You could put all your messages in a folder. You could put all the stuff that sets up and interacts with the bus in a file like MessageBus.js. You could put all your universal responses (like to logging in) in action files like actions/Authentication.js.
Then, when your application starts up, it might look like this:

import { getBus, publish, subscribe } from "./MessageBus.js";

import { registrar as registerAuthenticationActions } from "./actions/Authentication.js";

import { message as REQUEST_LOGIN } from "./messages/REQUEST_LOGIN.js";

var bus = getBus();
var partiallyAppliedPublish = ( message ) => publish( message, bus );
var partiallyAppliedSubscribe = ( map ) => subscribe( map, bus );

registerAuthenticationActions( partiallyAppliedSubscribe, partiallyAppliedPublish );

// More stuff?
/*
	Automatically log in with a demo user on startup?

	publish( Object.assign(
		{},
		REQUEST_LOGIN,
		{ "username": "demo", "password": "demopwd" }
	), bus );
*/

Footgun

This architecture is not for the faint of heart. A big part of the draw of something like Redux or Flux is it tells you what to do all the way down to the UI layer. Then, you're expected to use React for the UI, and that framework will also tell you what to do.

If you don't feel like there's enough structure here for you, you're probably right.

Fluxbus expects that you will exercise careful, defensive programming techniques. Because you have the freedom to make an enormous mess of things, you are expected not to.

Note - of course - there's no talk of data stores or dispatchers or reducers. Those are all the unnecessarily cumbersome bits of Redux/Flux. You want a data store? Great! Find one you like, or just use the many extremely robust storage options built into the browser. Do you need a way to reduce changes to data into your store? I suggest Object.assign( {}, previous, new ); but literally the whole world is open to you.