JavaScript and TypeScript course Β· Module 9: Design Patterns and Architecture
Event-driven architecture and time systems in JavaScript
In this lesson9
After mastering design patterns, SOLID principles, functional programming, and bundle optimization, it's time to combine this knowledge in practice. In this chapter, we will build an event-driven system with time simulation - a pattern commonly used in games, simulations, and monitoring systems.
What is event-driven architecture?
Event-driven architecture is an approach where program flow is controlled by events - user actions, sensor signals, time passage, or messages from other systems. Instead of linear code execution, the system reacts to events as they occur.
1// Traditional approach - linear
2function checkPark() {
3 checkFences();
4 feedDinosaurs();
5 updateVisitors();
6 generateReport();
7}
8
9// Event-driven - reactive
10const eventBus = new EventEmitter();
11
12eventBus.on('fence:alarm', handleFenceAlarm);
13eventBus.on('dinosaur:hungry', scheduleFeedingRun);
14eventBus.on('visitor:entered', updateVisitorCount);
15eventBus.on('hour:changed', generateHourlyReport);In the event-driven approach, each component operates independently and reacts only to events that concern it. This leads to loose coupling - a goal that the SOLID principles you learned earlier also serve, especially Dependency Inversion.
The EventEmitter pattern
At the heart of event-driven architecture is EventEmitter - an implementation of the Observer pattern that we learned in the first lesson of this module.
1class EventEmitter {
2 constructor() {
3 this.listeners = new Map();
4 }
5
6 on(event, callback) {
7 if (!this.listeners.has(event)) {
8 this.listeners.set(event, []);
9 }
10 this.listeners.get(event).push(callback);
11 return this; // Fluent interface
12 }
13
14 off(event, callback) {
15 if (!this.listeners.has(event)) return this;
16 const callbacks = this.listeners.get(event);
17 const index = callbacks.indexOf(callback);
18 if (index > -1) callbacks.splice(index, 1);
19 return this;
20 }
21
22 emit(event, ...args) {
23 if (!this.listeners.has(event)) return false;
24 // Copy the array, because once() removes a listener during the loop
25 [...this.listeners.get(event)].forEach(callback => {
26 callback(...args);
27 });
28 return true;
29 }
30
31 once(event, callback) {
32 const wrapper = (...args) => {
33 this.off(event, wrapper); // unregister first, then call
34 callback(...args);
35 };
36 return this.on(event, wrapper);
37 }
38}The once method is an elegant example of the Decorator pattern - it wraps the original callback in a wrapper that automatically unregisters itself after the first invocation.
Time simulation - day/night cycle
In simulation systems, time is a key element. In Jurassic Park, the day/night cycle affects dinosaur behavior, feeding schedules, visiting hours, and security protocols.
1class TimeSimulation extends EventEmitter {
2 constructor() {
3 super();
4 this.currentHour = 6; // Start at 6:00
5 this.day = 1;
6 this.scheduledEvents = [];
7 this.isRunning = false;
8 }
9
10 advanceHour() {
11 this.currentHour++;
12
13 if (this.currentHour >= 24) {
14 this.currentHour = 0;
15 this.day++;
16 this.emit('day:changed', this.day);
17 }
18
19 this.emit('hour:changed', this.currentHour, this.day);
20
21 // Check scheduled events
22 this.checkScheduledEvents();
23
24 // Emit only when the time of day changes
25 const timeOfDay = this.getTimeOfDay();
26 if (timeOfDay !== this.timeOfDay) {
27 this.timeOfDay = timeOfDay;
28 this.emit('timeOfDay:changed', timeOfDay);
29 }
30 }
31
32 getTimeOfDay() {
33 if (this.currentHour >= 6 && this.currentHour < 12) return 'morning';
34 if (this.currentHour >= 12 && this.currentHour < 18) return 'day';
35 if (this.currentHour >= 18 && this.currentHour < 22) return 'evening';
36 return 'night';
37 }
38
39 scheduleEvent(hour, callback, options = {}) {
40 const event = {
41 hour,
42 callback,
43 recurring: options.recurring || false,
44 label: options.label || 'unnamed'
45 };
46 this.scheduledEvents.push(event);
47 return event;
48 }
49
50 checkScheduledEvents() {
51 this.scheduledEvents = this.scheduledEvents.filter(event => {
52 if (event.hour === this.currentHour) {
53 event.callback(this.currentHour, this.day);
54 return event.recurring; // Keep only recurring ones
55 }
56 return true;
57 });
58 }
59}Notice how the TimeSimulation class inherits from EventEmitter - this is an application of the Open/Closed principle from SOLID. We can extend simulation behavior by adding new listeners without modifying the class itself.
Scheduling recurring events
In a real system, many events repeat cyclically - feeding dinosaurs, security patrols, status reports.
1const sim = new TimeSimulation();
2
3// Feeding carnivores - daily at 7:00 and 17:00
4sim.scheduleEvent(7, (hour, day) => {
5 console.log(`Day ${day}, ${hour}:00 - Feeding carnivores`);
6}, { recurring: true, label: 'feeding-carnivores' });
7
8sim.scheduleEvent(17, (hour, day) => {
9 console.log(`Day ${day}, ${hour}:00 - Second feeding for carnivores`);
10}, { recurring: true, label: 'feeding-carnivores-2' });
11
12// Security patrols - every hour at night (22:00-5:00)
13for (const h of [22, 23, 0, 1, 2, 3, 4, 5]) {
14 sim.scheduleEvent(h, (hour, day) => {
15 console.log(`Day ${day}, ${hour}:00 - Night patrol`);
16 }, { recurring: true, label: `patrol-${h}` });
17}
18
19// Daily report - at 8:00
20sim.scheduleEvent(8, (hour, day) => {
21 console.log(`Day ${day} - Generating daily report`);
22}, { recurring: true, label: 'daily-report' });Reacting to times of day
Event-driven architecture allows different subsystems to react independently to the same change:
1// Lighting system
2sim.on('timeOfDay:changed', (timeOfDay) => {
3 const lightingProfiles = {
4 morning: { brightness: 70, color: 'warm-white' },
5 day: { brightness: 100, color: 'daylight' },
6 evening: { brightness: 40, color: 'amber' },
7 night: { brightness: 10, color: 'red' } // Red so as not to startle dinosaurs
8 };
9 const profile = lightingProfiles[timeOfDay];
10 console.log(`Lighting: ${profile.brightness}% (${profile.color})`);
11});
12
13// Security system
14sim.on('timeOfDay:changed', (timeOfDay) => {
15 if (timeOfDay === 'night') {
16 console.log('Activating night security protocols');
17 console.log('Increasing fence voltage by 20%');
18 console.log('Enabling thermal cameras');
19 } else if (timeOfDay === 'morning') {
20 console.log('Deactivating night protocols');
21 console.log('Restoring standard fence voltage');
22 }
23});
24
25// Visitor system
26sim.on('hour:changed', (hour) => {
27 if (hour === 9) console.log('Park open for visitors');
28 if (hour === 18) console.log('Last visitor admission');
29 if (hour === 20) console.log('Park closed - guest evacuation');
30});Combining with functional programming
Event-driven architecture can be enriched with functional techniques we learned earlier in the module:
1// Pure functions for data transformation
2const calculateFeedingAmount = (species, weight, hour) => {
3 const baseAmount = weight * 0.02; // 2% of body mass
4 const timeMultiplier = hour < 12 ? 1.2 : 0.8; // More in the morning
5 return Math.round(baseAmount * timeMultiplier);
6};
7
8// Function composition for event processing
9const pipe = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
10
11const processAlert = pipe(
12 (alert) => ({ ...alert, timestamp: Date.now() }),
13 (alert) => ({ ...alert, severity: alert.level > 7 ? 'critical' : 'warning' }),
14 (alert) => ({ ...alert, notified: true })
15);
16
17// Usage with event system
18sim.on('alert:triggered', (rawAlert) => {
19 const processedAlert = processAlert(rawAlert);
20 console.log(`Alert [${processedAlert.severity}]: ${processedAlert.message}`);
21});Managing simulation state
We combine the state management pattern (exercise_9_4) with the event system:
1class SimulationState {
2 constructor(simulation) {
3 this.state = {
4 parkStatus: 'operational',
5 activeAlerts: 0,
6 dinosaursCount: 0,
7 visitorsCount: 0,
8 fencesStatus: 'all-operational'
9 };
10 this.history = [];
11
12 // React to simulation events
13 simulation.on('hour:changed', (hour) => {
14 this.updateState({ lastUpdate: hour });
15 });
16
17 simulation.on('alert:triggered', () => {
18 this.updateState({
19 activeAlerts: this.state.activeAlerts + 1
20 });
21 });
22 }
23
24 updateState(changes) {
25 // Immutability - new state object
26 const previousState = { ...this.state };
27 this.state = { ...this.state, ...changes };
28 this.history.push({
29 timestamp: Date.now(),
30 previous: previousState,
31 current: { ...this.state }
32 });
33 }
34
35 getState() {
36 return Object.freeze({ ...this.state });
37 }
38}Testing event-driven systems
Event-driven systems are easy to test thanks to loose coupling of components:
1// Time simulation test
2function testTimeSimulation() {
3 const sim = new TimeSimulation();
4 const events = [];
5
6 sim.on('hour:changed', (hour) => events.push({ type: 'hour', hour }));
7 sim.on('day:changed', (day) => events.push({ type: 'day', day }));
8
9 // Simulate 25 hours (going through an entire day)
10 for (let i = 0; i < 25; i++) {
11 sim.advanceHour();
12 }
13
14 console.log(`Recorded events: ${events.length}`);
15 console.log(`Day changes: ${events.filter(e => e.type === 'day').length}`);
16 console.log(`Current day: ${sim.day}, hour: ${sim.currentHour}`);
17}
18
19testTimeSimulation();Summary
Event-driven architecture combines many patterns and principles that you learned in this module:
- Observer (exercise 9_1) - foundation of the event system
- SOLID (exercise 9_2) - loose coupling, openness to extensions
- Functional programming (exercise 9_3) - pure functions for event processing
- State management (exercise 9_4) - reactive state updates
- Optimization (exercise 9_5-9_7) - a small bundle and subsystems loaded only when they are needed
This is preparation for the final project, where you will integrate all these elements into a complete Jurassic Park management system.
Code for this lesson: index.js
1// Time Simulation in the Park
2// Exercise: Implement a day/night cycle simulation
3
4class TimeSimulation {
5 constructor() {
6 this.currentHour = 6;
7 this.day = 1;
8 this.events = [];
9 this.isRunning = false;
10 }
11
12 // Implement the methods:
13 // advanceHour() - advance time by one hour
14 // scheduleEvent(hour, callback) - schedule an event
15 // getTimeOfDay() - return the time of day (morning/day/evening/night)
16
17 advanceHour() {
18 // Your code here
19 }
20
21 scheduleEvent(hour, callback) {
22 // Your code here
23 }
24
25 getTimeOfDay() {
26 // Your code here
27 }
28}
29
30const sim = new TimeSimulation();
31console.log("Simulation: Day", sim.day, "Hour:", sim.currentHour);Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What characterizes event-driven architecture?
2. What three basic methods should an EventEmitter implement?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Horizontal ordering
Arrange the elements of an EventEmitter.on call in the correct order:
- Code editor
The index.js file has the EventEmitter from the event-driven architecture lesson and a ParkTimeSystem class that simulates the hours in the park. Fill in the blanks: ___BLANK1___ is the array method that removes one listener at the found index in off(), ___BLANK2___ is the class that ParkTimeSystem extends to get the on, off and emit methods, and ___BLANK3___ is the array method that picks from the scheduled events only those for the current hour. Every hour advanceHour() emits 'hour:changed' with the hour and the day, after midnight first 'day:changed' with the day number, and every event scheduled with schedule(hour, name) for that hour emits 'event:triggered' with the name and the hour. start(intervalMs) starts a setInterval loop, and stop() stops it.
- Vertical ordering
Arrange the stages of building an event-driven system:
- Click in order
Arrange testing levels from lowest to highest.