DEV Community

koushikmaya
koushikmaya

Posted on

Building a Notification System with JavaScript EventEmitter and a Queue

If you are learning JavaScript, you have probably come across things
like events, callbacks, arrays, and DOM manipulation separately.

I recently worked through a small notification system that brought all
of these concepts together. At first, the code looked a little
complicated, especially the EventEmitter part. But once I broke it
down, the whole idea became much easier to understand.

This blog explains the same system in a beginner-friendly way.


What are we trying to build?

Imagine a social media application.

Three things happen:

  • Alex follows you
  • Sam likes your post
  • John comments on your post

We want to show notifications like:

Alex followed you
Sam liked your post
John commented on your post
Enter fullscreen mode Exit fullscreen mode

The important part is that these notifications should appear one after
another
, not all at once.

That gives us two main problems to solve:

  1. How do we know when an event happens?
  2. How do we display notifications in the correct order?

The solution is:

EventEmitter + Notification Queue


1. Understanding Events

An event is simply something that happens.

For example:

Someone followed me
Someone liked my post
Someone commented on my post
Enter fullscreen mode Exit fullscreen mode

We can give these events names:

"new-follower"
"like"
"comment"
Enter fullscreen mode Exit fullscreen mode

Instead of directly connecting every part of our application together,
we can say:

"When this event happens, run this function."

This is where an EventEmitter becomes useful.


2. Creating the EventEmitter

Here is the basic structure:

class EventEmitter {
    constructor() {
        this.events = {};
    }
}
Enter fullscreen mode Exit fullscreen mode

A class is basically a blueprint.

When we write:

const notificationEmitter = new EventEmitter();
Enter fullscreen mode Exit fullscreen mode

we create an actual EventEmitter object.

The constructor runs automatically and creates:

this.events = {};
Enter fullscreen mode Exit fullscreen mode

At this point, events is just an empty object:

{}
Enter fullscreen mode Exit fullscreen mode

We will use it to store our event listeners.


3. What is this?

If this is your first time working with classes, this can be
confusing.

When we create:

const notificationEmitter = new EventEmitter();
Enter fullscreen mode Exit fullscreen mode

this refers to the object being created.

So:

this.events = {};
Enter fullscreen mode Exit fullscreen mode

is essentially giving our new object an events property.

Conceptually:

notificationEmitter.events = {};
Enter fullscreen mode Exit fullscreen mode

So our object starts like this:

notificationEmitter
        |
        └── events
              |
              └── {}
Enter fullscreen mode Exit fullscreen mode

4. Listening to an Event with on()

Now we need a way to listen for events.

That's what the on() method does:

on(eventName, callback) {
    if (!this.events[eventName]) {
        this.events[eventName] = [];
    }

    this.events[eventName].push(callback);
}
Enter fullscreen mode Exit fullscreen mode

For example:

emitter.on("like", handleLike);
Enter fullscreen mode Exit fullscreen mode

This means:

"When the like event happens, remember handleLike."

Here:

"like"
Enter fullscreen mode Exit fullscreen mode

is the event name.

And:

handleLike
Enter fullscreen mode Exit fullscreen mode

is the callback.


5. What is a Callback?

A callback is simply a function that we give to another function so it
can be called later.

For example:

function handleLike() {
    console.log("Someone liked the post");
}

emitter.on("like", handleLike);
Enter fullscreen mode Exit fullscreen mode

We don't write:

emitter.on("like", handleLike());
Enter fullscreen mode Exit fullscreen mode

because that would execute the function immediately.

Instead:

emitter.on("like", handleLike);
Enter fullscreen mode Exit fullscreen mode

means:

"Here is the function. Run it when the like event occurs."


6. Why do we use an Array?

Inside on() we have:

if (!this.events[eventName]) {
    this.events[eventName] = [];
}
Enter fullscreen mode Exit fullscreen mode

Suppose we listen for "like" for the first time.

Initially:

this.events = {};
Enter fullscreen mode Exit fullscreen mode

There is no "like" property.

So we create:

this.events["like"] = [];
Enter fullscreen mode Exit fullscreen mode

Now we can add callbacks:

this.events["like"].push(callback);
Enter fullscreen mode Exit fullscreen mode

Eventually, the object could look like:

{
    like: [functionA, functionB],
    comment: [functionC],
    "new-follower": [functionD]
}
Enter fullscreen mode Exit fullscreen mode

The array is useful because multiple functions can listen to the same
event.


7. Stopping a Listener with off()

Sometimes we don't want a function to listen anymore.

That's what off() does:

off(eventName, callback) {
    if (!this.events[eventName]) {
        return;
    }

    this.events[eventName] =
        this.events[eventName].filter(
            (listener) => listener !== callback
        );
}
Enter fullscreen mode Exit fullscreen mode

Suppose we have:

emitter.on("like", handleLike);
Enter fullscreen mode Exit fullscreen mode

Later, we can remove it:

emitter.off("like", handleLike);
Enter fullscreen mode Exit fullscreen mode

The filter() method creates a new array that keeps every listener
except the one we want to remove.

For example:

Before:

[functionA, functionB, functionC]

Remove functionB

After:

[functionA, functionC]
Enter fullscreen mode Exit fullscreen mode

So the easy way to remember it is:

on  → start listening
off → stop listening
Enter fullscreen mode Exit fullscreen mode

8. Triggering an Event with emit()

Now comes the most important part.

emit(eventName, data) {
    if (!this.events[eventName]) {
        return;
    }

    this.events[eventName].forEach((callback) => {
        callback(data);
    });
}
Enter fullscreen mode Exit fullscreen mode

emit() means:

"This event happened."

For example:

emitter.emit("like");
Enter fullscreen mode Exit fullscreen mode

means:

"A like happened!"

We can also send information with the event:

emitter.emit("like", {
    name: "Sam"
});
Enter fullscreen mode Exit fullscreen mode

Now the callback receives:

{
    name: "Sam"
}
Enter fullscreen mode Exit fullscreen mode

as its data.


9. How emit() Finds the Right Function

Suppose we registered:

emitter.on("like", (data) => {
    console.log(`${data.name} liked your post`);
});
Enter fullscreen mode Exit fullscreen mode

Then we trigger:

emitter.emit("like", {
    name: "Sam"
});
Enter fullscreen mode Exit fullscreen mode

The EventEmitter looks inside:

this.events["like"]
Enter fullscreen mode Exit fullscreen mode

and finds the callback.

Then:

forEach()
Enter fullscreen mode Exit fullscreen mode

goes through the listeners.

Finally:

callback(data);
Enter fullscreen mode Exit fullscreen mode

runs the callback and passes the data to it.

The result is:

Sam liked your post
Enter fullscreen mode Exit fullscreen mode

10. Connecting the EventEmitter to Notifications

Now we can connect the EventEmitter to our notification system.

For a new follower:

notificationEmitter.on(
    "new-follower",
    (data) => {
        addNotification(
            `${data.name} followed you`
        );
    }
);
Enter fullscreen mode Exit fullscreen mode

For a like:

notificationEmitter.on(
    "like",
    (data) => {
        addNotification(
            `${data.name} liked your post`
        );
    }
);
Enter fullscreen mode Exit fullscreen mode

For a comment:

notificationEmitter.on(
    "comment",
    (data) => {
        addNotification(
            `${data.name} commented on your post`
        );
    }
);
Enter fullscreen mode Exit fullscreen mode

Now the EventEmitter doesn't need to know how notifications are
displayed.

It simply triggers the appropriate callback.

The callback creates the notification message.


11. The Notification Queue

Now we have another problem.

What happens if three notifications arrive at almost the same time?

We don't want:

Alex followed you
Sam liked your post
John commented
Enter fullscreen mode Exit fullscreen mode

to all fight for the same space on the screen.

Instead, we use a queue:

const notificationQueue = [];
Enter fullscreen mode Exit fullscreen mode

A queue works like a line.

The first notification that enters should be the first one displayed.

This is called:

FIFO --- First In, First Out

For example:

Alex
Sam
John
Enter fullscreen mode Exit fullscreen mode

Alex goes first, then Sam, then John.


12. Adding Notifications to the Queue

Our function is:

function addNotification(message) {
    notificationQueue.push(message);

    showNextNotification();
}
Enter fullscreen mode Exit fullscreen mode

The first line:

notificationQueue.push(message);
Enter fullscreen mode Exit fullscreen mode

adds the notification to the end of the queue.

For example:

[]
Enter fullscreen mode Exit fullscreen mode

Then:

["Alex followed you"]
Enter fullscreen mode Exit fullscreen mode

Then:

[
    "Alex followed you",
    "Sam liked your post"
]
Enter fullscreen mode Exit fullscreen mode

And finally:

[
    "Alex followed you",
    "Sam liked your post",
    "John commented"
]
Enter fullscreen mode Exit fullscreen mode

So:

push()
Enter fullscreen mode Exit fullscreen mode

means:

Add something to the end.


13. Taking the First Notification

When it's time to display a notification, we use:

const message = notificationQueue.shift();
Enter fullscreen mode Exit fullscreen mode

shift() removes the first item.

For example:

[
    "Alex followed you",
    "Sam liked your post",
    "John commented"
]
Enter fullscreen mode Exit fullscreen mode

After shift():

message = "Alex followed you"
Enter fullscreen mode Exit fullscreen mode

And the queue becomes:

[
    "Sam liked your post",
    "John commented"
]
Enter fullscreen mode Exit fullscreen mode

This is what gives us FIFO behavior.

Remember:

push()  → add to the end
shift() → remove from the beginning
Enter fullscreen mode Exit fullscreen mode

14. Making Sure Only One Notification Shows

We use a simple flag:

let isShowingNotification = false;
Enter fullscreen mode Exit fullscreen mode

This tells us whether a notification is currently visible.

When nothing is showing:

false
Enter fullscreen mode Exit fullscreen mode

When a notification is on screen:

true
Enter fullscreen mode Exit fullscreen mode

This prevents multiple notifications from appearing at the same time.


15. The Important if Statement

Inside showNextNotification() we have:

if (
    isShowingNotification ||
    notificationQueue.length === 0
) {
    return;
}
Enter fullscreen mode Exit fullscreen mode

This basically asks two questions:

  1. Is a notification already showing?
  2. Is the queue empty?

If either answer is yes, we stop.

The || means OR.

So in plain English:

"If something is already showing OR there is nothing waiting in the
queue, don't do anything."

This small check is what keeps the queue under control.


16. Creating the Notification in the DOM

Once we're ready to display a notification:

const notification = document.createElement("div");
Enter fullscreen mode Exit fullscreen mode

This creates a new <div> using JavaScript.

Then:

notification.className = "notification-toast";
Enter fullscreen mode Exit fullscreen mode

gives it a CSS class.

And:

notification.textContent = message;
Enter fullscreen mode Exit fullscreen mode

puts the notification message inside it.

Finally:

container.appendChild(notification);
Enter fullscreen mode Exit fullscreen mode

adds it to the webpage.

If the HTML contains:

<div id="notification-container"></div>
Enter fullscreen mode Exit fullscreen mode

we can end up with:

<div id="notification-container">
    <div class="notification-toast">
        Sam liked your post
    </div>
</div>
Enter fullscreen mode Exit fullscreen mode

17. Removing the Notification

We don't want the notification to stay forever.

So we use:

setTimeout(() => {
    notification.remove();

    isShowingNotification = false;

    showNextNotification();
}, 3000);
Enter fullscreen mode Exit fullscreen mode

The 3000 means 3000 milliseconds, or 3 seconds.

So the process is:

Show notification
      ↓
Wait 3 seconds
      ↓
Remove notification
      ↓
Set isShowingNotification to false
      ↓
Show the next notification
Enter fullscreen mode Exit fullscreen mode

This is what makes the notifications appear one after another.


18. Putting Everything Together

Now the complete flow makes much more sense.

Suppose we run:

notificationEmitter.emit(
    "new-follower",
    {
        name: "Alex Developer"
    }
);
Enter fullscreen mode Exit fullscreen mode

The flow is:

emit()
  ↓
Find "new-follower" listener
  ↓
Run callback
  ↓
Create "Alex Developer followed you"
  ↓
addNotification()
  ↓
push() into queue
  ↓
showNextNotification()
  ↓
Display notification
Enter fullscreen mode Exit fullscreen mode

Then:

notificationEmitter.emit(
    "like",
    {
        name: "Sam"
    }
);
Enter fullscreen mode Exit fullscreen mode

creates:

Sam liked your post
Enter fullscreen mode Exit fullscreen mode

If Alex's notification is still visible, Sam's notification waits in the
queue.

Then the comment event creates:

John commented on your post
Enter fullscreen mode Exit fullscreen mode

That also waits.

The final queue might look like:

[
    "Sam liked your post",
    "John commented on your post"
]
Enter fullscreen mode Exit fullscreen mode

Once Alex's notification disappears, Sam appears.

Then John appears.


19. The Full Mental Model

The easiest way I found to understand the project is to think of it as
two systems working together.

EventEmitter

Something happens
       ↓
emit()
       ↓
Find listeners
       ↓
Run callback
Enter fullscreen mode Exit fullscreen mode

Notification Queue

Callback creates message
       ↓
push()
       ↓
Wait in queue
       ↓
shift()
       ↓
Display notification
       ↓
Wait 3 seconds
       ↓
Remove notification
       ↓
Show next one
Enter fullscreen mode Exit fullscreen mode

Together:

User action
    ↓
Event
    ↓
emit()
    ↓
EventEmitter
    ↓
callback
    ↓
addNotification()
    ↓
Queue
    ↓
showNextNotification()
    ↓
DOM
    ↓
3 seconds
    ↓
Remove
    ↓
Next notification
Enter fullscreen mode Exit fullscreen mode

20. What I Learned from This

The biggest lesson for me was that the code looks much more complicated
when everything is viewed at once.

Breaking it into smaller ideas makes it easier:

  • class creates the EventEmitter blueprint.
  • on() stores listeners.
  • off() removes listeners.
  • emit() triggers listeners.
  • callback is the function that runs when an event occurs.
  • push() adds a notification to the queue.
  • shift() takes the oldest notification out.
  • setTimeout() controls how long the notification stays visible.
  • DOM methods create and remove the notification on the page.

Once these pieces are understood individually, the whole notification
system becomes much easier to follow.


Final Takeaway

You don't need to memorize the entire code.

If you're learning this from scratch, focus on these five ideas first:

on()       → listen
emit()     → trigger
callback   → respond
push()     → add to queue
shift()    → take from queue
Enter fullscreen mode Exit fullscreen mode

That's the foundation of this notification system.

And honestly, that's one of the useful things about building small
JavaScript projects: concepts that seem abstract in isolation start
making sense when you see them working together.

Top comments (0)