Design Patterns: Singleton & Factory
What Are Design Patterns?
Design patterns are proven, reusable solutions to common problems in software design. They are not code you copy-paste -- they are templates for solving recurring design challenges. In this lesson, we will explore two of the most fundamental creational patterns: the Singleton (ensuring only one instance exists) and the Factory (delegating object creation to specialized methods).
The Singleton Pattern
A Singleton ensures that a class has exactly one instance throughout the entire application, and provides a global access point to that instance. Use it when you need a single shared resource -- like a database connection pool, a logger, or an app configuration.
Singleton with Factory Constructor (Dart Idiom)
class AppConfig {
// 1. Private static field holds the single instance
static final AppConfig _instance = AppConfig._internal();
// 2. Factory constructor always returns the SAME instance
factory AppConfig() {
return _instance;
}
// 3. Private named constructor -- called only once
AppConfig._internal();
// Instance fields
String apiBaseUrl = 'https://api.example.com';
bool debugMode = false;
int maxRetries = 3;
void configure({String? apiUrl, bool? debug, int? retries}) {
if (apiUrl != null) apiBaseUrl = apiUrl;
if (debug != null) debugMode = debug;
if (retries != null) maxRetries = retries;
}
}
void main() {
// Both variables point to the SAME object
final config1 = AppConfig();
final config2 = AppConfig();
print(identical(config1, config2)); // true -- same instance!
config1.configure(apiUrl: 'https://prod.api.com', debug: true);
print(config2.apiBaseUrl); // https://prod.api.com
print(config2.debugMode); // true -- changes reflect everywhere
}
factory keyword tells Dart this constructor does not always create a new instance. Instead, it returns _instance, which was created once by the private _internal() constructor. Every call to AppConfig() returns the same object.Lazy Singleton
The example above is an eager singleton -- the instance is created when the class loads. Sometimes you want a lazy singleton that is only created when first accessed, especially if initialization is expensive.
Lazy Singleton Pattern
class DatabaseConnection {
// Instance is null until first access
static DatabaseConnection? _instance;
// Track connection state
final String connectionString;
bool _isConnected = false;
// Factory constructor with lazy initialization
factory DatabaseConnection(String connStr) {
// Create the instance only on first call
_instance ??= DatabaseConnection._internal(connStr);
return _instance!;
}
DatabaseConnection._internal(this.connectionString);
Future<void> connect() async {
if (_isConnected) return;
print('Connecting to $connectionString...');
// Simulate connection delay
await Future.delayed(Duration(milliseconds: 100));
_isConnected = true;
print('Connected!');
}
bool get isConnected => _isConnected;
}
void main() async {
// First call creates the instance
final db1 = DatabaseConnection('postgresql://localhost/mydb');
await db1.connect();
// Second call returns the SAME instance (argument is ignored)
final db2 = DatabaseConnection('mysql://other/db');
print(identical(db1, db2)); // true
print(db2.connectionString); // postgresql://localhost/mydb
print(db2.isConnected); // true
}
'mysql://other/db') is silently ignored because the instance already exists. This is a common source of confusion. If your singleton needs different configurations, consider using a separate initialize() method instead of passing arguments to the constructor.Singleton with Static Getter
An alternative approach uses a static getter with the late keyword for lazy initialization without a factory constructor:
Static Getter Singleton
class Logger {
// late + static = lazy initialization
static late final Logger instance = Logger._();
Logger._();
final List<String> _logs = [];
void log(String message) {
final timestamp = DateTime.now().toIso8601String();
final entry = '[$timestamp] $message';
_logs.add(entry);
print(entry);
}
void warning(String message) => log('WARNING: $message');
void error(String message) => log('ERROR: $message');
List<String> get history => List.unmodifiable(_logs);
void clear() => _logs.clear();
}
void main() {
// Access via the static getter
Logger.instance.log('App started');
Logger.instance.warning('Low memory');
// Same instance everywhere
final logger = Logger.instance;
logger.error('Network failure');
print('Total logs: ${Logger.instance.history.length}'); // 3
}
The Factory Method Pattern
The Factory Method pattern delegates object creation to a method instead of calling a constructor directly. This lets you return different subtypes based on input, hide complex creation logic, and make your code easier to extend.
Simple Factory with Static Method
abstract class Notification {
final String title;
final String body;
Notification(this.title, this.body);
void send();
// Factory method -- returns the right subtype
static Notification create(String channel, String title, String body) {
return switch (channel) {
'email' => EmailNotification(title, body),
'sms' => SmsNotification(title, body),
'push' => PushNotification(title, body),
_ => throw ArgumentError('Unknown channel: $channel'),
};
}
}
class EmailNotification extends Notification {
EmailNotification(super.title, super.body);
@override
void send() => print('EMAIL: "$title" -- $body');
}
class SmsNotification extends Notification {
SmsNotification(super.title, super.body);
@override
void send() => print('SMS: "$title" -- $body');
}
class PushNotification extends Notification {
PushNotification(super.title, super.body);
@override
void send() => print('PUSH: "$title" -- $body');
}
void main() {
// Caller does not need to know which class to instantiate
final channels = ['email', 'sms', 'push'];
for (final ch in channels) {
final notification = Notification.create(ch, 'Hello', 'Welcome!');
notification.send();
}
// EMAIL: "Hello" -- Welcome!
// SMS: "Hello" -- Welcome!
// PUSH: "Hello" -- Welcome!
}
Abstract Factory Pattern
The Abstract Factory takes the concept further -- it defines an interface for creating families of related objects. This is useful when you need to create multiple related objects that must be consistent with each other (like UI themes or platform-specific widgets).
Abstract Factory -- Theme System
// Abstract products
abstract class Button {
void render();
}
abstract class TextField {
void render();
}
abstract class Card {
void render();
}
// Abstract factory
abstract class ThemeFactory {
Button createButton(String label);
TextField createTextField(String placeholder);
Card createCard(String title, String content);
// Factory method to get the right theme
static ThemeFactory getTheme(String theme) {
return switch (theme) {
'material' => MaterialThemeFactory(),
'cupertino' => CupertinoThemeFactory(),
_ => throw ArgumentError('Unknown theme: $theme'),
};
}
}
// Material Design family
class MaterialButton extends Button {
final String label;
MaterialButton(this.label);
@override
void render() => print('[Material Button: $label]');
}
class MaterialTextField extends TextField {
final String placeholder;
MaterialTextField(this.placeholder);
@override
void render() => print('[Material TextField: $placeholder]');
}
class MaterialCard extends Card {
final String title, content;
MaterialCard(this.title, this.content);
@override
void render() => print('[Material Card: $title - $content]');
}
class MaterialThemeFactory extends ThemeFactory {
@override
Button createButton(String label) => MaterialButton(label);
@override
TextField createTextField(String ph) => MaterialTextField(ph);
@override
Card createCard(String t, String c) => MaterialCard(t, c);
}
// Cupertino (iOS) family
class CupertinoButton extends Button {
final String label;
CupertinoButton(this.label);
@override
void render() => print('[Cupertino Button: $label]');
}
class CupertinoTextField extends TextField {
final String placeholder;
CupertinoTextField(this.placeholder);
@override
void render() => print('[Cupertino TextField: $placeholder]');
}
class CupertinoCard extends Card {
final String title, content;
CupertinoCard(this.title, this.content);
@override
void render() => print('[Cupertino Card: $title - $content]');
}
class CupertinoThemeFactory extends ThemeFactory {
@override
Button createButton(String label) => CupertinoButton(label);
@override
TextField createTextField(String ph) => CupertinoTextField(ph);
@override
Card createCard(String t, String c) => CupertinoCard(t, c);
}
void main() {
// Change this one line to switch the entire UI theme
final factory = ThemeFactory.getTheme('material');
final btn = factory.createButton('Submit');
final input = factory.createTextField('Enter email');
final card = factory.createCard('Welcome', 'Hello there!');
btn.render(); // [Material Button: Submit]
input.render(); // [Material TextField: Enter email]
card.render(); // [Material Card: Welcome - Hello there!]
}
MaterialThemeFactory, all your buttons, text fields, and cards will be Material-styled. You cannot accidentally mix Material buttons with Cupertino text fields. In Flutter, this concept underpins the difference between MaterialApp and CupertinoApp.Anti-Patterns to Avoid
Design patterns are powerful, but misusing them creates more problems than they solve:
Common Anti-Patterns
// ANTI-PATTERN 1: Singleton for everything
// BAD -- making a UserService a singleton creates tight coupling
class UserService {
static final UserService _i = UserService._();
factory UserService() => _i;
UserService._();
// This is hard to test! You cannot mock or replace it.
Future<User> getUser(int id) { /* ... */ }
}
// BETTER -- use dependency injection
class UserService {
final ApiClient _client;
UserService(this._client); // inject the dependency
Future<User> getUser(int id) => _client.get('/users/$id');
}
// ANTI-PATTERN 2: God Factory
// BAD -- one factory that creates unrelated objects
class AppFactory {
static Object create(String type) {
return switch (type) {
'user' => User(),
'logger' => Logger(),
'database' => Database(),
'button' => Button(),
_ => throw Error(),
};
}
}
// BETTER -- separate factories for related families
class NotificationFactory { /* email, sms, push */ }
class ThemeFactory { /* buttons, inputs, cards */ }
// ANTI-PATTERN 3: Factory returning concrete type
// BAD -- no benefit over calling constructor directly
class UserFactory {
static User create(String name) => User(name);
// This adds indirection with zero benefit
}
// Factories are useful when:
// - You return different subtypes based on input
// - Creation involves complex logic
// - You want to cache/reuse instances
get_it, provider, or riverpod over raw Singletons. These tools give you the benefits of single instances while keeping your code testable and modular.