
The Singleton guarantees that a class has only one instance and provides a global access point to it. In modern JavaScript, the simplest way is a private static field and a getInstance() method: static #instance; static getInstance() { return Database.#instance ??= new Database(); }. An ES module that exports an object is already a singleton, and is often the best answer.
The Singleton is probably the best-known and most debated design pattern. Its promise is simple: a class that can have only one instance, accessible from anywhere. A database connection, a logger or the application’s configuration are the classic examples. In JavaScript it takes a few lines, and the language even offers a free alternative: the module. Here are the implementations that work today, along with their pitfalls.
The Singleton in one sentence
A Singleton prevents the creation of several instances of a class and provides a single access point to the existing instance. The first call creates the object; the following ones return the same one. You use it when a resource genuinely has to be shared: opening ten connections to the same database because ten modules called new Database() would be wasteful, and a log split across ten files would be unusable.
It is discouraged as soon as it only serves to avoid passing a parameter: it then becomes a global variable in disguise, hard to test and to replace. We come back to this at the end of the article.
Implementation 1: a class with a private static field
This is the recommended modern form. The #instance field is private and static: it belongs to the class, not to the objects, and nobody can read or overwrite it from outside. getInstance() creates the instance on the first call thanks to the ??= operator (“assign if nullish”).
class Database {
static #instance;
constructor(dsn) {
this.dsn = dsn;
this.connectedAt = new Date();
}
static getInstance(dsn = 'mongodb://localhost') {
Database.#instance ??= new Database(dsn);
return Database.#instance;
}
query(sql) {
return `${this.dsn} > ${sql}`;
}
}
const a = Database.getInstance('mongodb://prod');
const b = Database.getInstance('mysql://autre');
console.log(a === b); // true : same object
console.log(b.dsn); // 'mongodb://prod' : the second dsn was ignoredThe last line illustrates a pitfall of the pattern: the parameters of the second call are silently lost. If your singleton takes parameters, either they come from a single source (the configuration), or getInstance() throws an error when it is given different ones.
Nothing yet prevents writing new Database() directly. JavaScript has no private constructor; you simulate one with a token known only to the class:
const cle = Symbol('Database');
class Database {
static #instance;
constructor(jeton) {
if (jeton !== cle) {
throw new Error('Utilisez Database.getInstance()');
}
}
static getInstance() {
return (Database.#instance ??= new Database(cle));
}
}
new Database(); // Error: Utilisez Database.getInstance()Implementation 2: the constructor returns the existing instance
This is the historical version from this article, and it still works: if a constructor explicitly returns an object, new returns that object instead of the new one. The instance is stored in a static property.
class Database {
constructor(data) {
if (Database.instance) {
return Database.instance; // new returns the existing object
}
this._data = data;
Database.instance = this;
}
getData() {
return this._data;
}
setData(data) {
this._data = data;
}
}
const mongo = new Database('mongo');
console.log(mongo.getData()); // 'mongo'
const mysql = new Database('mysql');
console.log(mysql.getData()); // 'mongo' : it is the same object as mongo
console.log(mongo === mysql); // trueIts advantage is that it leaves the new Database() syntax untouched for the calling code. Its drawback is that it surprises: a new that creates nothing goes against the intuition of whoever reads the code, and Database.instance is public, hence modifiable. The private-field version is more explicit.
Implementation 3: an ES module is already a singleton
A JavaScript module is evaluated only once per application, however many files import it. Exporting an instance is therefore enough to get a singleton, with no class and no getInstance().
const config = Object.freeze({
env: process.env.NODE_ENV ?? 'development',
apiUrl: 'https://api.example.com',
});
export default config;class Logger {
#lines = [];
log(message) {
this.#lines.push(`${new Date().toISOString()} ${message}`);
}
get count() {
return this.#lines.length;
}
}
export const logger = new Logger(); // a single instance for the whole applicationimport { logger } from './logger.js';
import config from './config.js';
logger.log(`Démarrage en ${config.env}`);Every file that imports logger receives the same object. It is the solution to prefer in most projects: it is readable, free of magic, and the module system guarantees uniqueness. Two limits: the instance is created when the module loads, even if nobody uses it, and the singleton is only unique per file path (two copies of a package in node_modules make two instances).
TypeScript version
TypeScript adds what JavaScript lacks: a genuinely private constructor, which forbids new at compile time.
class Database {
private static instance: Database | undefined;
private constructor(private readonly dsn: string) {}
static getInstance(dsn = 'mongodb://localhost'): Database {
Database.instance ??= new Database(dsn);
return Database.instance;
}
query(sql: string): string {
return `${this.dsn} > ${sql}`;
}
}
const db = Database.getInstance();
// new Database('x'); // Error TS2673: the constructor is privateThe PHP equivalent
The structure is identical: a static property, a private constructor, a getInstance() method. PHP adds two useful locks, a private __clone and a __wakeup that refuses deserialisation, so that no copy can ever appear.
final class Database
{
private static ?Database $instance = null;
private function __construct(private readonly string $dsn)
{
}
public static function getInstance(string $dsn = 'mysql:host=localhost'): Database
{
return self::$instance ??= new self($dsn);
}
public function query(string $sql): string
{
return "{$this->dsn} > {$sql}";
}
private function __clone()
{
}
public function __wakeup(): void
{
throw new LogicException('Un singleton ne se désérialise pas.');
}
}
$a = Database::getInstance('mysql:host=prod');
$b = Database::getInstance();
var_dump($a === $b); // bool(true)
echo $a->query('SELECT 1'); // mysql:host=prod > SELECT 1When not to use a Singleton
The pattern is criticised for good reasons, and you should know them before adopting it:
- It hides dependencies. A function that calls
Database.getInstance()in the middle of its code depends on the database without saying so in its signature. Passing the connection as a parameter (dependency injection) makes the dependency visible and replaceable. - It complicates tests. State survives from one test to the next. If you keep a singleton, add a
static reset()method reserved for tests, or test through injection. - It is global state. Everything that is true of global variables is true of the singleton: coupling, action at a distance, initialisation order.
The practical rule: a module that exports an instance (implementation 3) for configuration and stateless utilities; dependency injection for anything that touches an external resource; and the getInstance() class when a library imposes that single access point. The guard clause pattern and classes in JavaScript extend this topic on the code-structure side.
Common errors
static reset() method reserved for tests, or inject the dependency rather than calling getInstance() in business code.new Database('mysql') returns the instance created with “mongo”: the arguments of the second call are lost without warning. Document it, or throw an error if the parameters differ.getInstance().

