04 May 2026
If you're not familiar with the first two points, this blog might not be suitable for you. Otherwise, you're good to go. The examples are intentionally simple and can be easily replicated in other languages.
Hey devs, welcome back to the design pattern series!
So far in our Booking System, we've covered all five Creational patterns:
Today we're going back to look at the one pattern we skipped very first one on the Creational list. The Singleton Pattern.
It's often called the simplest pattern. And in some ways, it is. But simple doesn't mean trivial. Get it wrong and you'll end up with bugs that are surprisingly hard to trace.
Let's break it down through our Booking System.
Singleton is a creational design pattern that ensures a class has only one instance and provides a global access point to that instance.
That's it. Two rules:
One instance. Everyone shares it.
The pattern controls instantiation. The class itself is responsible for making sure no second copy ever gets created. And it hands out the same instance every time someone asks for one.
Let's think about our AvailabilityCache.
The availability cache stores which rooms, seats, or vehicles are currently free. Every time a user searches for available resources, the system hits this cache instead of going straight to the database. It's a performance layer and a critical one.
Now imagine what happens if multiple parts of the system each create their own instance of AvailabilityCache.
Three caches. Three different versions of the truth. Which one is right?
what goes wrong here?
Stale data across instances One part of the system marks Room 101 as booked. But another part of the system is reading from its own separate cache instance that hasn't been updated. Room 101 looks available there. Double booking incoming.
Wasted memory Each instance holds its own copy of the entire availability dataset. That is not a small object. Multiply it by however many services are spinning up their own cache, and you have got a memory problem nobody asked for.
No single source of truth When availability changes, a booking is confirmed, or a cancellation comes in - only one cache gets the update. The rest stay out of sync. Your system is now lying to itself.
Without Singleton:
class AvailabilityCache {
private cache: Map<string, boolean> = new Map();
isAvailable(resourceId: string): boolean {
return this.cache.get(resourceId) ?? true;
}
markAsBooked(resourceId: string): void {
this.cache.set(resourceId, false);
}
}
// Different parts of the system doing this independently
const cacheA = new AvailabilityCache(); // used by SearchService
const cacheB = new AvailabilityCache(); // used by BookingService
const cacheC = new AvailabilityCache(); // used by AdminService
// cacheA says Room 101 is available
// cacheB just marked Room 101 as booked
// cacheA still says available — race condition territory
Left: three caches, three sources of truth. Right: one cache, one source of truth.
This is exactly the problem the Singleton pattern is designed to solve.
The fix is straightforward. Make AvailabilityCache responsible for its own instantiation. Give it a private constructor so nothing outside can call new AvailabilityCache(). Then expose a static method that always returns the same instance.
Everyone uses the same machine. Everyone gets the same item.
class AvailabilityCache {
private static instance: AvailabilityCache | null = null;
private cache: Map<string, boolean> = new Map();
private constructor() {
// Private constructor - prevents direct instantiation
// Initialize your cache here if needed
}
static getInstance(): AvailabilityCache {
if (!AvailabilityCache.instance) {
AvailabilityCache.instance = new AvailabilityCache();
}
return AvailabilityCache.instance;
}
isAvailable(resourceId: string): boolean {
return this.cache.get(resourceId) ?? true;
}
markAsBooked(resourceId: string): void {
this.cache.set(resourceId, false);
}
markAsAvailable(resourceId: string): void {
this.cache.set(resourceId, true);
}
getSnapshot(): Map<string, boolean> {
return new Map(this.cache);
}
}
Now no matter where in the system you call AvailabilityCache.getInstance(), you always get the exact same object back.
// SearchService
const cache = AvailabilityCache.getInstance();
cache.isAvailable("room-101"); // true
// BookingService — same instance
const cache = AvailabilityCache.getInstance();
cache.markAsBooked("room-101");
// SearchService checks again — sees the update
const cache = AvailabilityCache.getInstance();
cache.isAvailable("room-101"); // false — correctly updated
The class holds a reference to itself. That is the whole trick.
In a single-threaded environment like a typical Node.js process, the basic implementation above is completely fine. Node.js runs on a single thread, so two calls cannot race to create two instances simultaneously.
But if you are ever working in a multi-threaded environment (Java, C#, Go with goroutines), you need to think about thread safety. Two threads could both reach the if (!instance) check at the same time before either has created the instance, and both end up creating one. The fix there is a lock or a double-checked locking pattern.
Since we are in TypeScript/Node.js here, we don't have that concern. Just good to know it exists.
The main example we've been building. Every service in the booking system shares one cache.
class SearchService {
private cache = AvailabilityCache.getInstance();
findAvailableRooms(date: string): string[] {
return roomIds.filter(id => this.cache.isAvailable(id));
}
}
class BookingService {
private cache = AvailabilityCache.getInstance();
confirmBooking(resourceId: string): void {
// Process payment, save to DB...
this.cache.markAsBooked(resourceId); // Same instance updated
}
}
When BookingService marks a room as booked, SearchService immediately sees it gone from available results. No sync required.
Configuration is another classic Singleton use case. Your system reads environment variables, feature flags, and settings once at startup. You want the same config everywhere, and you definitely don't want it re-read from disk or env on every service instantiation.
class BookingConfig {
private static instance: BookingConfig | null = null;
readonly maxBookingsPerUser: number;
readonly cancellationWindowHours: number;
readonly supportedCurrencies: string[];
private constructor() {
this.maxBookingsPerUser = parseInt(process.env.MAX_BOOKINGS ?? "5");
this.cancellationWindowHours = parseInt(process.env.CANCEL_WINDOW ?? "24");
this.supportedCurrencies = (process.env.CURRENCIES ?? "USD,EUR,GBP").split(",");
}
static getInstance(): BookingConfig {
if (!BookingConfig.instance) {
BookingConfig.instance = new BookingConfig();
}
return BookingConfig.instance;
}
}
// Anywhere in the system
const config = BookingConfig.getInstance();
if (booking.currency && !config.supportedCurrencies.includes(booking.currency)) {
throw new Error("Unsupported currency");
}
Both Singletons are shared across all services. One call, same instance every time.
| Benefit | Why it matters |
|---|---|
| Single source of truth | All parts of the system read and write to the same object |
| Controlled instantiation | The class manages its own lifecycle, no accidents |
| Memory efficient | One instance in memory instead of N copies |
| Global access | No need to pass the object around through every layer |
| Lazy initialization | Instance is created only when first needed, not at startup |
| Drawback | Why it matters |
|---|---|
| Hard to test | Global state makes unit tests interfere with each other |
| Hidden dependencies | Code that uses getInstance() hides its dependency from the outside |
| Violates Single Responsibility | The class manages both its own logic and its own instantiation |
| Difficult to subclass | Private constructor makes extending the class complicated |
| Can mask design problems | Sometimes a Singleton is used where proper dependency injection would be cleaner |
The Singleton pattern solves the "one instance per process" problem. It does not solve the "one instance across all servers" problem. Know the difference before reaching for it.
AvailabilityCache and BookingConfigOk devs, that's it for today. I tried my best to explain this pattern. We have wrapped up all five Creational patterns in our Booking System:
| Pattern | Role in Booking System |
|---|---|
| Singleton | One shared AvailabilityCache and BookingConfig |
| Factory Method | Create the right PaymentProcessor by type |
| Abstract Factory | Build entire booking families (Standard, Holiday, Corporate) |
| Builder | Construct complex bookings step by step |
| Prototype | Clone existing bookings instead of recreating from scratch |
Next up, we move into Structural Patterns. The first one is the Adapter Pattern and we'll use it to wrap a third-party payment API so it fits cleanly into our IPaymentProcessor interface without touching the core system. See you there!
Code, learn, refactor, repeat