A story about reactive thinking

Back in 2014, Max and Simon thought about starting a business to maximise user experience with modern web development. When clients came up with an idea, they did everything by themselves (including research, estimations, design, development, marketing and accounting). But soon they realised that they needed to change their structure to deal with the increasing work load. This story is about modelling of their company in a reactive way.

Reactive Terms

Before we start refactoring, let’s clarify some terms up front. In the reactive world there are observables. These observables are a stream of data. One can subscribe to that stream and will then receive all the new items passed into the stream. Imagine you are watching TV. Then you receive the current image and sound as a hot stream. Hot means that it is even active when you do not watch or listen to the stream. Whereas when you watch Netflix the stream is cold. It starts only if you click on a movie and emits only that movie. Observables are cold by default. In application terms a hot observable can be a stream that listens to user inputs like clicks on specific elements. A cold observable for example can be an http request to a web server. These observables are by convention prepended by $ sign in the code.

In addition to these behaviour, observables are chain-able. Each observable has a pipe member function. It can handle so called operators as its arguments. You can think of an operator as a modification observable, but very important: As an observable. The observable is a class that looks like

class Observable {
producer;

constructor(producer) {
this.producer = producer;
}
  subscribe(observer) {
return this.producer(observer);
}
  pipe(...operators) {
return operators.reduce((source, next) => next(source), this);
}
}

In this class you can notice that the observable only executes the producer function if the subscription is executed. In addition to that, the pipe function receives a list of operators which will be executed by step by step with each step as the source of the next step.

The client, the company and the app

In RxJS software terms the client is an observable that modifies the environment stream. If something changes in the environment, like the market situation, the client is going to receive it automatically. Based on this information he builds up a new application idea and figures out its requirements and the available budget.

const client$ = environment$.pipe(
map(environment => {
// analyze the market
// find a new idea based on the market knowledge
// return {idea, requirements, budget}

})

The map function takes a mapping function. This function maps the environment to other properties based on the developers needs. The company of Max and Simon currently looks like the following observable. They are just two people and have to manage every task by themselves. This results in a huge lists of tasks and can become very messy.

const company$ = zip(
clients$,
environment$
).pipe(
map(([{ requirements, idea, budget }, environment]) => {
// reject or accept the idea based on
// environment, budget and requirements
// transform the idea to a PoC
// estimate risks and time schedule
// destructure problem to small tickets
// implement designs and software
// return an application

})
);

When the application is completed, it will be forwarded to the testers before it will be published in the Playstore. The testers receive the application and the clients requirements and check if every part is sophisticated. If it is not ok, it will reject the application and do not emit it to further subscriptions.

const requirements$ = clients$.pipe(pluck('requirements'));
const testers$ = zip(
requirements$,
company$
).pipe(
filter(([requirements, application]) =>
application.matches(requirements)
)
);

The filter function takes a function that checks if the emitted values (requirements and application) should continue the stream based on true or false values. The playstore should add (or overwrite) new applications (or versions) in the playstore. In addition to that, all customers browsing inside the appstore (subscribe to the store) should receive the same value independently of how many people currently watching the store.

const playstore$ = testers$.pipe(
scan((allApps, app) => ({...allApps, [app.id]: app}), {}),
shareReplay(1)
);

This is the complete process of the image from above in a reactive way of programming. If there is a change in the environment$ the data is going to pass through all these steps reactively. To complete the idea we need to know that every process step only makes sense, if there is at least one subscriber to the store, which means there is at least one subscriber to each observable in the observable chain. Otherwise all parts of the process would not be executed. This subscription process is a very important pattern in reactive programming and is going to be discussed in the following section in detail. Keep in mind that this process might not reflect an actual real-world scenario.

The steps of subscribing

Before we continue building our decoupled stream, it is time to go for a little foray. In the common way of explaining RxJS this progress is not mentioned very often. How subscriptions work: imagine environments$ is a http call to an API which can be requested to receive details about companies, peoples interests and country wealth and then complete. A custom observable making an XHR request would look like

function get(url: string): Observable {
const producer = (observer: Observer) => {
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
if (xhr.status === 200 && xhr.readyState === 4) {
observer.next(JSON.parse(xhr.responseText));
observer.complete();
}
});
    xhr.open('GET', url);
xhr.send();
    return () => xhr.abort();
}
  return new Observable(producer);
}

We can use this observable in the RxJS way by defining an observer with the next, error and complete function:

environments$ = get(`https://publicapi.com`).subscribe({
next: (data) => console.log(data),
error: (err) => console.log(err),
complete: () => console.log('The stream has completed');
});

of course with an imaginary URL. If we subscribe to this observable, it executes the observables so called producer function, which is in this case the XMLHttpRequest and returns the data to the stream with the observers next function, then completes. The subscription method receives either an observer or three callbacks in exactly the same order as above. Let’s take a look on operators and create a custom one.

function map(mapOperation) {
return (source) => {
const producer = (observer) => source.subscribe({
next: (value) => observer.next(mapOperation(value))
});
    return new Observable(producer);
}
}

In this example map returns a function that has a source argument and returns a new observable. The observables producer function subscribes to the source observable and returns each source observables value with the mapFn applied. You can have a look at the origin RxJS map operator and you will see the similarities. Now we can use the pipe function to chain this observable to the http observable.

const currentTechnologies$ = get('https://publicapi.com').pipe(
map((environment) => environment.currentTechnologies)
);
currentTechnologies$.subscribe({
next: (technologies) => console.log(technologies),
complete: () => console.log('Stream has completed')
})

So what does the subscription do? You can imagine each entry in a pipe as another observable. It subscribes to the observable that was returned by the map operator first, and afterwards this observable subscribes to the http observable. Then the actual http process is going to be executed (because it is a cold observable and only executes on subscription). If the data arrives it is going to be processed inside the map operator and afterwards it is accessible in the subscription. In addition to that, the http request observable completes and sends it complete state to the map and cuts the subscription between those two. Then the map emits the complete state and the complete function inside the subscription fires. The subscription between the subscribe and the map is now gone as well.

The subscription process is a key concept that we should keep in mind for the rest of the article and whenever we are going to work with RxJS. Let’s continue with the application building process. It is time to get more people involved.

Time to hire

Developers

When time was passing by and more clients are asking for software, the founders Max and Simon thought that it would be nice to have a real expert on the technology they want to choose for most of their projects. Also they thought that it was good to have a more structured way for their organisation. The only requirement which must be met is that the company receives an idea and payments from the client and outputs an application. So they decided to hire Kris, an expert Angular developer.

const founders$ = zip(
client$,
environment$
).pipe(
map(([{idea, payments}, env]) => // Todo
// reject or accept
// transform the idea based on the environment to
// a proof of concept
// estimate risks
// return parts of the payments, prototype and estimation
)
)

As you can see, the both founders Max and Simon significantly reduced their workload and they can focus on accounting, client acquisition and business development. From now on they do not need to know the complete development steps in depth. This is where Kris comes into play.

const developers = ['Kris'];
const speedOfDevelopment = developers.length / 1000;
const developers$ = founders$.pipe(
filter(({payments}) => payments > 0),
map(({payments, prototype, estimation}) => // Todo
// create subtasks
// implement designs
// implement application
// return application
),
delay(1 / speedOfDevelopment)
)

He is responsible for creating the application and also for the designs. As more and more projects come up, they decide to reduce the time needed to finish a project by hiring more developers.

const developers = ['Kris', 'Anna', 'Gustav', 'Yens', 'Maria', 'Lennard'];

But hiring more and more developers was not enough. So Max and Simon decided to search for some project leads to manage projects, estimate the schedule and break down the implementation tasks into smaller subtasks. Also Kris wished for some designers to tackle the design tasks.

What have you recognised alongside with the implementation of the developers and? We have decoupled our code of one observable into smaller pieces which are way more simple to handle and less error prune than the big one. And that is a core assumption of reactive programming: you divide bigger parts of the program into smaller parts that act independently. This is what we also want to do with project leads in our example.

Project Leads

The developers did a very good work, but they were a bit unorganised and sometimes they found themselves in finding the best of the best solution fanatically, instead of having a look at the time and focus on the most important parts. Here came the project leads into play. They are in charge of making an overview of all projects and create small tickets for each task. In addition to that, if there are new developers subscribing to the project lead, they should also get the latest tickets.

const projectLeads = ['Emma', 'Markus'];
const projectLeads$ = founders$.pipe(
scan(({ payments, prototype, estimation }) => // Todo
// Append application to either Emma's or Markus'
// application stack
// Append prototype and estimation to the project
// leads internal projects list
// Check if project payment ressources are covered
, {}),
map(({ projects }) => // Todo
// Create small tickets for the developers and
// (later on) designers
// Estimate a time for each ticket
// Return tickets
),
shareReplay(1)
);

The developer’s implementation now needs to be refactored to receive the tickets instead of the full application.

const developers$ = projectLeads$.pipe(
map(ticket => // Todo
// Implement designs
// Implement application
// Return application
)
);

Designers

At this point the applications are technically top-notch, reactive, and use best-practices and latest technologies. Max and Simon are quite satisfied with the results, but there is one key-point missing. The applications look pretty straight forward and like every other developer-only application. Mostly with a titlebar, a side navigation, some cards and a table. So the leads decide to hire some designers to raise the user experience to the next level.

const designers = ['Agathe', 'Annabelle', 'David'];
const designers$ = this.projectLead$.pipe(
filter(ticket => ticket.isForDesigners),
map(ticket => //Todo
// Create design based on tickets
// Return design
)
);

The developers now can use these pretty designs to implement first-class looking applications.

const developers$ = this.projectLeads$.pipe(
filter(ticket => ticket.isForDevelopers),
withLatestFrom(this.designers$),
map(([ticket, designs]) => // Todo
// Implement features/application
// Return application
)
);