Capstone Integration Patterns

Introduction: From Individual Tools To Integrated Systems

In our previous lessons, we explored powerful meta-programming tools in isolation. We saw how the Proxy object and Reflect API allow us to intercept and change how objects behave. We also learned how Tagged Template Literals can turn simple strings into specialized tools like secure SQL query builders. While these tools are impressive on their own, their true power is unlocked when we combine them to build a cohesive system.

In this final lesson, we are going to build a Capstone project that integrates everything we have learned. To do this, we need an architectural spine to hold all these pieces together. We will use the Observable pattern. This pattern allows different parts of our application to stay in sync without being directly connected to one another. By the end of this lesson, you will understand how to design objects that are not just data containers, but smart entities that broadcast changes, support custom protocols, and work seamlessly with functional programming helpers.

On the CodeSignal IDE, you will find all the modern JavaScript features we use are ready to go. While you can use these patterns in any environment, here you can focus entirely on the logic of the integration. Let's begin by looking at the core of our system: the Observable.

The Observable Pattern: Events Without Coupling

The Observable pattern, also known as publish/subscribe, is a design where an object maintains a list of listeners and notifies them automatically of any state changes. This is a vital pattern in modern software because it allows us to keep our code decoupled. This means the object sending the update doesn't need to know who is listening or what they will do with the information.

To implement this safely, we use a private #subscribers Set. Using a Set ensures that the same listener function cannot be added twice. The subscribe method first validates that the argument is a function, throwing a TypeError if it is not. This prevents subtle bugs that would otherwise surface only at notification time. When validation passes, we return an unsubscribe closure. This is a small function that remembers the listener and the Set, allowing the user to stop receiving updates by simply calling that returned function. This approach is much cleaner than exposing the internal list of listeners to the outside world, as it protects our data from being accidentally changed by other parts of the program.

When it comes time to send an update, the notify method iterates over a copy of the subscribers. We create this copy using the spread operator [...this.#subscribers]. This is a defensive programming technique. If a listener tries to unsubscribe right in the middle of receiving a notification, iterating over a copy prevents our loop from breaking or skipping other listeners. Each listener call is also wrapped in a try/catch block. This ensures that if one listener throws an error, the remaining listeners still receive the notification. The error is logged to the console so it is not silently swallowed.

class Observable {
  #subscribers = new Set();

  subscribe(fn) {
    if (typeof fn !== "function") throw new TypeError("Subscriber must be a function");
    this.#subscribers.add(fn);
    return () => this.#subscribers.delete(fn);
  }

  notify(event) {
    for (const fn of [...this.#subscribers]) {
      try { fn(event); } catch (e) { console.error("Listener error:", e); }
    }
  }
}

const bus = new Observable();
const received = [];
const off = bus.subscribe((e) => received.push(e));

bus.notify("hello");
off();
bus.notify("ignored");

console.log("received:", received);

In the output below, you can see that only the first message was saved because we used the off switch returned by the subscribe method before the second message was sent.

received: ["hello"]
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal