In the first article of this series, we looked at why traditional communication patterns on the frontend lead to unnecessary coupling. We introduced the event bus as an alternative, and we defined the criteria for a good implementation: type safety, automatic subscription management, support for both fire‑and‑forget and request‑response, synchronous executors, and middleware.
Now it’s time to see the simplest – and often most useful – type of event bus communication - fire‑and‑forget events. These are messages that carry information from a publisher to any number of subscribers, with no expectation of a reply. The publisher does not wait, does not care who receives the message, and does not receive any result. It simply announces that something happened.
This pattern is perfect for notifications, UI updates in unrelated parts of the screen, analytics, logging, and any situation where “one component does something, and others may react”.
What Is a Fire‑and‑Forget Event?
Formally, a fire‑and‑forget event is a message that:
- Is sent into the event bus.
- Can be received by zero, one, or many subscribers.
- Does not return any value to the sender.
- Does not block or wait.
From the publisher’s perspective:
// Publisher – no return value, no waiting
postboy.fire(new CartItemAddedEvent(item));
From the subscriber’s perspective:
// Subscriber – receives the event and reacts
postboy.sub(CartItemAddedEvent).subscribe(ev => {
console.log(`Added ${ev.item.quantity} of ${ev.item.productId}`);
updateBadge(ev.item.quantity);
});
No promises, no observables chaining (from the publisher side), no callbacks. Just a typed message and a clean separation of concerns.
A Complete Example: Shopping Cart Notifications
Imagine an e‑commerce application. When a user adds an item to the cart, several parts of the UI need to react:
- The cart icon badge should increment.
- A toast notification should appear: “Item added”.
- Analytics should record the event.
- A wishlist button (if the item was previously saved) should change its state.
Using traditional methods, the component that adds the item would have to know about all of these dependencies or fire multiple events through a shared service. With an event bus, it only needs to fire one event. Everything else becomes a separate, loosely coupled subscriber.
Step 1. Define the Event
import {PostboyGenericMessage} from '@artstesh/postboy';
export interface CartItem {
productId: string;
quantity: number;
price: number;
}
import {PostboyGenericMessage} from '@artstesh/postboy';
export class CartItemAddedEvent extends PostboyGenericMessage {
public static readonly ID = '7f3a2b1c-8e9d-4f5a-9b2c-1a2b3c4d5e6f';
constructor(public readonly item: CartItem) {
super();
}
}
The static ID ensures that even if the class name changes or minification renames it, the event type remains unique. This is a robust design for production.
Step 2. Publish the Event
@Component({...})
export class ProductDetailComponent {
constructor(private postboy: AppPostboyService) {
}
addToCart(item: CartItem) {
// ... some business logic
this.postboy.fire(new CartItemAddedEvent(item));
// the event is now in the bus
}
}
Notice that the component does not import any other subscriber. It does not know about the badge, the toast, or analytics. This makes it trivial to test: you can verify that postboy.fire was called with the correct event, without setting up a dozen mocks.
Step 3. Subscribe in Different Places
Cart badge component:
@Component({...})
export class CartBadgeComponent implements OnInit, OnDestroy {
private subscription?: Subscription;
public count = 0;
constructor(private postboy: AppPostboyService) {
}
ngOnInit() {
this.subscription = this.postboy.sub(CartItemAddedEvent).subscribe(event => {
this.count += event.item.quantity;
});
}
ngOnDestroy() {
this.subscription?.unsubscribe();
}
}
Toast notification service:
import {IPostboyDependingService} from "@artstesh/postboy";
@Injectable({...})
export class ToastService implements IPostboyDependingService {
constructor(private postboy: AppPostboyService) {
}
up(): void {
this.postboy.sub(CartItemAddedEvent).subscribe(event => {
this.show(`Added ${event.item.quantity} x ${event.item.productId}`);
});
}
}
Analytics service:
import {IPostboyDependingService} from "@artstesh/postboy";
@Injectable({...})
export class AnalyticsService implements IPostboyDependingService {
constructor(private postboy: AppPostboyService) {
}
up(): void {
this.postboy.sub(CartItemAddedEvent).subscribe(event => {
this.track('add_to_cart', {productId: event.item.productId, quantity: event.item.quantity});
});
}
}
Each subscriber lives in its own module, can be developed and tested independently, and can be added or removed without any change to the publisher.
Visualising the Flow
The following PlantUML diagram shows the decoupled architecture:
No arrows between the components themselves. The bus is the only “hub”. This is the essence of loose coupling.
When to Use Fire‑and‑Forget (and When Not To)
Good fits
- UI coordination – Closing a modal when a form is submitted, refreshing a list after data changes.
- Notifications – Toast messages, snackbars, system alerts.
- Analytics / logging – Any “record that something happened” scenario.
- Cross‑cutting concerns – User login/logout events, theme changes, language changes.
- Broadcasting to dynamic listeners – A dashboard with widgets that can appear/disappear.
Not a good fit
- State synchronisation – If two components must always show the same value and that value changes often, a shared store or reactive state is better. Events are for actions, not for state.
- High‑frequency events – Mouse move, scroll, etc. An event bus adds overhead; use direct DOM or framework events.
Automatic Lifecycle Management: A Key Advantage
One of the pain points with hand‑rolled Subjects or shared services is manual unsubscription. Forgetting to unsubscribe causes memory leaks and unexpected behaviour. A good event bus provides tools to manage subscriptions automatically.
Using namespaces (as in @artstesh/postboy)
@Component({...})
export class ProductListComponent implements OnInit, OnDestroy {
private namespace = 'ProductList';
public count = 0;
constructor(private postboy: AppPostboyService) {
postboy.exec(new AddNamespace('ProductList'))
.recordSubject(ShowProductDetailsCommand);
}
ngOnInit() {
// the event lyfecycle is linked to the namespace
this.postboy.sub(ShowProductDetailsCommand).subscribe(cmd => {
// open a modal with product details
});
// other events from higher scopes work as usual
this.postboy.sub(CartItemAddedEvent).subscribe(event => {
this.count += event.quantity;
});
}
ngOnDestroy() {
this.postboy.exec(new ElimitateNamespace('ProductList')); // unsubscribes all
}
}
The lifecycle concepts are (described in the library documentation), the bus allows you to group messages, executors, and even entire services that depend on the bus. This eliminates whole classes of memory leak bugs.
What happens if you forget to unsubscribe?
Because the bus uses RxJS Subjects internally, every subscription adds a listener that stays in memory. If the component is destroyed but the subscription remains, the handler will still be called – and will try to update a DOM element that no longer exists, or access a service that has been cleaned up. Automatic lifecycle management is not a luxury; it is a necessity for long‑living applications.
Testing Fire‑and‑Forget Events
Because the bus is injected and all communication goes through it, testing becomes straightforward.
Testing the publisher (ProductDetailComponent)
You can mock the bus and verify that fire() was called with the correct event with the @artstesh/postboy-testing - a special library for testing the event bus (you can read more about it in the library documentation).
import {PostboyWorld} from "@artstesh/postboy-testing";
import {Forger} from '@artstesh/forger';
import { should } from '@artstesh/it-should';
describe('ProductDetailComponent', () => {
let world: PostboyWorld;
let component: ProductDetailComponent;
beforeEach(() => {
world = new PostboyWorld();
component = new ProductDetailComponent(world.postboy);
});
it('should fire CartItemAddedEvent when addToCart is called', async () => {
const item = Forger.create<CartItem>()!;
//
component.addToCart(item);
//
let firedEvent = await world.waiter.waitFor(CartItemAddedEvent, {includeHistory: true});
should().string(firedEvent.productId).equals(item.productId);
});
});
This test checks that calling addToCart on a ProductDetailComponent correctly fires a CartItemAddedEvent. The test uses @artstesh/forger to create a mock CartItem, triggers the addToCart method, and then waits for the expected event via world.waiter.waitFor. Finally, it asserts that the productId in the fired CartItemAddedEvent matches the productId of the original item, confirming the component behaves as expected with minimal setup.
No need to mock the badge, toast, or analytics – they are not part of the test.
Testing a subscriber (ProductListComponent)
You can fire events directly into the bus and verify the component’s reaction.
import {PostboyWorld} from "@artstesh/postboy-testing";
import {Forger} from '@artstesh/forger';
import { should } from '@artstesh/it-should';
describe('ProductDetailComponent', () => {
let world: PostboyWorld;
beforeEach(() => {
world = new PostboyWorld();
});
it('should increment product count when CartItemAddedEvent arrives', () => {
const item = Forger.create<CartItem>()!;
world.given.event(new CartItemAddedEvent(item));
//
const component = new ProductListComponent(world.postboy);
//
should().number(component.count).equals(item.quantity);;
});
});
This test demonstrates the ease of testing with @artstesh/postboy. It verifies that a ProductListComponent correctly updates its count property when a CartItemAddedEvent is received. The test uses @artstesh/forger to create a mock CartItem, dispatches the event via world, and asserts that the component’s state matches the item’s quantity.
Common Patterns with Fire‑and‑Forget
1. Debouncing rapid events
Sometimes many events fire in quick succession (e.g., typing in a search box). You can debounce them at the subscriber side using RxJS operators:
this.postboy.sub(SearchInputEvent).pipe(
debounceTime(300),
distinctUntilChanged((a, b) => a.query === b.query)
).subscribe(event => {
// perform search
});
2. Filtering events
You may want only events that satisfy a condition (e.g., only high‑value cart additions):
this.postboy.sub(CartItemAddedEvent).pipe(
filter(event => event.price * event.quantity > 100)
).subscribe(event => {
// show "free shipping eligible" message
});
3. Taking the first event only (e.g., initialization)
this.postboy.sub(AppInitializedEvent).pipe(
first()
).subscribe(() => {
this.loadInitialData();
});
These patterns are natural because the event bus exposes an RxJS Observable for each message type. You can use the full power of reactive programming without any extra glue.
Performance Considerations
A common question: “Does an event bus slow down my application?”
For typical UI events (user clicks, form submissions, API responses) – absolutely not. The overhead of passing a message through a Subject is negligible compared to DOM updates or network requests.
If you have thousands of events per second (e.g., streaming data), you may want to be careful. But in practice, such high‑frequency scenarios are better handled with direct streams or specialised WebSocket handling. For 99% of frontend use cases, the bus is fast enough.
The real performance gain is in developer productivity – less time debugging tangled dependencies, faster feature additions, and easier refactoring.
What’s Next?
In this article we focused on fire‑and‑forget events – the simplest, most common form of event bus communication. You have seen:
- How to define and fire a generic message.
- How to subscribe and react, with automatic lifecycle management.
- When to use this pattern (and when to avoid it).
- How to test publishers and subscribers in isolation.
The next article will cover request‑response (callback) messages – for those cases where the publisher needs an answer. We will see how to model commands and queries, handle errors, cancel pending requests, and compose results from multiple sources.
If you want to experiment right now, you can take any small Angular, React, or Vue project and introduce a simple event bus. Define one event, fire it from a child component, and subscribe in an unrelated component. You will immediately feel the reduction in coupling.
And if you prefer to use a ready‑made, type‑safe implementation, @artstesh/postboy provides all the features described here and more. But as always – the pattern is what matters, not the library.
Next article: Callback Messages – Asynchronous Queries and Commands
Thank you for reading. Feedback and questions are always welcome.

Top comments (0)