Inside and Outside

Messages flowing into and within the boundaries of a bus

Andrew Gibson
4 min readOct 7, 2023
A bus can create a sense of “internal” and “external” contexts

Previously, we reached an awkward half way point.

We sent our Cart messages describing events in time.

import { Cart } from "./Cart.js";

const cart = Cart();

// 2 yoyos
cart(Message(AddedToCart, { …yoyo, quantity: 2 }));

// 1 skpping rope
cart(Message(AddedToCart, { …skippingRope, quantity: 1 }));

// checkout
const cartTotals = cart(Message(AtCheckout));
console.log(cartTotals.items, "items, total cost:", cartTotals.total);

But, we coupled our script to cart’s implementation of handling AtCheckout.

Instead of relying on messages, we implied a contract. We depended on the cart to return a cartTotals message. In effect we commanded the cart to calculate the totals for us.

Message passing allows us to react to messages instead of commanding an action:

import shopping from "./objects/shopping.js";

// 2 yoyos
shopping(AddedToCart({ ...yoyo, quantity: 2 }));

// 1 skpping rope
shopping(AddedToCart({ ...skippingRope, quantity: 1 }));

// checkout
shopping(AtCheckout());

Now we don’t even have direct access to the cart object. But we do have access to a shopping object.

Inside and Outside

Our shopping object represents a boundary of sorts. From outside, we can send messages to the object, but other objects exist inside the boundary:

// objects/shopping.js

import { Cart } from “../factories/Cart.js”;
import { Bus } from "../factories/Bus.js";
import { Checkout } from "../factories/Checkout.js";

export default Bus([
Cart(),
Checkout()
]);

Unbeknownst to other objects, our shopping object is a Bus. Outwardly, it receives messages just like all our other objects.

Internally, it contains one of our original Cart objects, and a new Checkout object.

Message flow within the bus

When the bus receives a message, it immediately does two things:

  1. it places the message in a queue.
  2. it executes a loop to process the queue.

Queue processing sends the message to each of the objects internal to the bus. But, it also places any resulting messages back into the queue.

message flow into and within the bus and the bus’s components

The bus continues to process messages until the queue empties.

// factories/Bus.js

export function Bus(components) {

const queue = [];
return message => {

queue.push(message);
processMessageQueue();

};

function processMessageQueue() {

while(queue.length) {

// next message to dispatch
const next = queue.shift();

// call all components with the next message;
const messages = components.flatMap(component => component(next));

// push all resulting messages on to the queue
queue.push(...messages.filter(x => x));

}

}

}

The Checkout

Our bus enabled the outside control logic to rely entirely on message dispatch. We send messages noting the addition of products to the cart. This results in other messages flowing within the bus.

The checkout object is responsible for the checkout process. But, to do this it listens out for another message flowing within the context of shopping.

// factories/Checkout.js 

import { AtCheckout, CartTotalsUpdated } from "../taxonomy.js";

export function Checkout() {

let latestTotals;

return message => {

switch (true) {
case message instanceof CartTotalsUpdated:
latestTotals = message;
break;
case message instanceof AtCheckout:
logCartTotals(latestTotals);
break;
}

};

}

function logCartTotals({ items, total }) {

console.log(items, "items, total cost:", total);

}

Any time it detects a CartTotalsUpdated message, it stores the message away in preparation. Finally, when the AtCheckout message finally arrives, it fulfils its responsibility.

Note that the Checkout object is entirely unaware of any Cart object. All it knows are the messages it can receive and send.

A cognitive boundary

Eric Evans, talking about Bounded Contexts, says

Model expressions, like any other phrase, only have meaning in context.

The same can be said of messages flowing within our bus. When creating this aggregate object, I chose a context boundary within which messages are well understood.

As this context grows, it might become harder to understand what a message means, what data it holds or what schema it uses. If it happens, this is a sign that we should rethink the divisions, or introduce new subdivisions.

Conclusion

We’re now free of dependencies between the different objects in our system. Each object is free to send and receive messages without any external contract, other than the messages themselves.

This is a loosely coupled system. And, using contexts like shopping we can create high cohesion too.

https://github.com/goofballLogic/modd/tree/article-2023/articles/003-OutsideAndInside

--

--

Andrew Gibson

Business and technology in the software engineering space