Manager Pattern
Centralize object lifecycle, lookup, and cleanup behind a dedicated manager class — from resource pools to entity managers and service registries.
Overview
The Manager Pattern puts the lifecycle, coordination, and access control of a set of related objects behind a dedicated manager class. Instead of letting every caller create, find, and destroy objects on its own, the manager owns those operations and enforces the rules around them.
This pattern shows up everywhere once you know what to look for, though it rarely carries the name. Entity managers in ORMs, connection pools in networking code, scene managers in game engines, service registries in operating systems — same idea, different domain. Think of the manager as the only front door to a collection of managed objects.
Without one, the failure modes are predictable: objects get created in three different places with slightly different setup, indexes drift out of sync with the primary store, and cleanup is forgotten in some code path nobody tests. Centralizing that logic in one class is what the pattern is for.
When to Use
Use the Manager Pattern when:
- A group of related objects needs centralized lifecycle management — creation, lookup, mutation, and removal all go through one place.
- Constraints or invariants have to hold across the whole collection — unique IDs, per-group caps, that kind of rule.
- Objects are looked up by ID or key rather than held by direct reference, and you want a canonical instance per key (an identity map).
- Resources need pooling, caching, or reuse — connections, threads, sprites, sessions.
- Several callers share the same collection and consistency between the store and its indexes matters.
When to Avoid
- A single object that never coordinates with peers doesn’t need a manager.
- When a plain collection or array is enough, extra structure just gets in the way.
- If the manager starts handling unrelated responsibilities, it’s become a God object — split it.
- When a DI container or service registry already gives you the same coordination, don’t build a parallel mechanism. See the Dependency Injection Pattern for that approach. For the web-tier variant that fronts managers, see the Business Delegate Pattern.
Solution
Python
from typing import Dict, List, Optional
from dataclasses import dataclass, field
import uuid
@dataclass
class Employee:
name: str
department: str
salary: float
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
active: bool = True
class EmployeeManager:
"""Centralizes lifecycle and operations for Employee objects"""
def __init__(self):
self._employees: Dict[str, Employee] = {}
self._department_index: Dict[str, List[str]] = {}
def hire(self, name: str, department: str, salary: float) -> Employee:
emp = Employee(name=name, department=department, salary=salary)
self._employees[emp.id] = emp
self._department_index.setdefault(department, []).append(emp.id)
return emp
def get(self, emp_id: str) -> Optional[Employee]:
return self._employees.get(emp_id)
def fire(self, emp_id: str) -> bool:
emp = self._employees.get(emp_id)
if emp:
emp.active = False
self._department_index[emp.department].remove(emp_id)
del self._employees[emp_id]
return True
return False
def list_by_department(self, department: str) -> List[Employee]:
ids = self._department_index.get(department, [])
return [e for e in (self._employees.get(eid) for eid in ids)
if e is not None and e.active]
def total_payroll(self) -> float:
return sum(emp.salary for emp in self._employees.values() if emp.active)
def give_raise(self, emp_id: str, percentage: float) -> bool:
emp = self._employees.get(emp_id)
if emp:
emp.salary *= (1 + percentage / 100)
return True
return False
# Usage
manager = EmployeeManager()
emp1 = manager.hire("Alice", "Engineering", 120000)
emp2 = manager.hire("Bob", "Engineering", 95000)
emp3 = manager.hire("Carol", "Sales", 80000)
print(f"Total payroll: ${manager.total_payroll():,.2f}")
print(f"Engineering team: {[e.name for e in manager.list_by_department('Engineering')]}")
manager.give_raise(emp1.id, 10)
print(f"Alice's new salary: ${manager.get(emp1.id).salary:,.2f}")
Java
import java.util.*;
class Employee {
private final String id;
private String name;
private String department;
private double salary;
private boolean active = true;
public Employee(String name, String department, double salary) {
this.id = UUID.randomUUID().toString().substring(0, 8);
this.name = name;
this.department = department;
this.salary = salary;
}
public String getId() { return id; }
public String getName() { return name; }
public String getDepartment() { return department; }
public double getSalary() { return salary; }
public void setSalary(double salary) { this.salary = salary; }
public boolean isActive() { return active; }
public void setActive(boolean active) { this.active = active; }
}
class EmployeeManager {
private final Map<String, Employee> employees = new HashMap<>();
private final Map<String, List<String>> deptIndex = new HashMap<>();
public Employee hire(String name, String department, double salary) {
Employee emp = new Employee(name, department, salary);
employees.put(emp.getId(), emp);
deptIndex.computeIfAbsent(department, k -> new ArrayList<>()).add(emp.getId());
return emp;
}
public Employee get(String id) { return employees.get(id); }
public boolean fire(String id) {
Employee emp = employees.get(id);
if (emp != null) {
emp.setActive(false);
deptIndex.getOrDefault(emp.getDepartment(), new ArrayList<>()).remove(id);
employees.remove(id);
return true;
}
return false;
}
public List<Employee> listByDepartment(String department) {
List<String> ids = deptIndex.getOrDefault(department, List.of());
List<Employee> result = new ArrayList<>();
for (String id : ids) {
Employee emp = employees.get(id);
if (emp != null && emp.isActive()) result.add(emp);
}
return result;
}
public double totalPayroll() {
return employees.values().stream()
.filter(Employee::isActive)
.mapToDouble(Employee::getSalary)
.sum();
}
}
// Usage
EmployeeManager manager = new EmployeeManager();
Employee alice = manager.hire("Alice", "Engineering", 120000);
manager.hire("Bob", "Engineering", 95000);
System.out.println("Total payroll: $" + manager.totalPayroll());
JavaScript
class Employee {
constructor(name, department, salary) {
this.id = Math.random().toString(36).substring(2, 10);
this.name = name;
this.department = department;
this.salary = salary;
this.active = true;
}
}
class EmployeeManager {
constructor() {
this.employees = new Map();
this.deptIndex = new Map();
}
hire(name, department, salary) {
const emp = new Employee(name, department, salary);
this.employees.set(emp.id, emp);
if (!this.deptIndex.has(department)) {
this.deptIndex.set(department, []);
}
this.deptIndex.get(department).push(emp.id);
return emp;
}
get(id) {
return this.employees.get(id);
}
fire(id) {
const emp = this.employees.get(id);
if (emp) {
emp.active = false;
const dept = this.deptIndex.get(emp.department) || [];
const idx = dept.indexOf(id);
if (idx > -1) dept.splice(idx, 1);
this.employees.delete(id);
return true;
}
return false;
}
listByDepartment(department) {
const ids = this.deptIndex.get(department) || [];
return ids.map(id => this.employees.get(id)).filter(e => e && e.active);
}
totalPayroll() {
let total = 0;
for (const emp of this.employees.values()) {
if (emp.active) total += emp.salary;
}
return total;
}
giveRaise(id, percentage) {
const emp = this.employees.get(id);
if (emp) {
emp.salary *= (1 + percentage / 100);
return true;
}
return false;
}
}
// Usage
const manager = new EmployeeManager();
const alice = manager.hire('Alice', 'Engineering', 120000);
manager.hire('Bob', 'Engineering', 95000);
manager.hire('Carol', 'Sales', 80000);
console.log('Total payroll:', manager.totalPayroll());
console.log('Engineering:', manager.listByDepartment('Engineering').map(e => e.name));
Explanation
The manager takes on four jobs that would otherwise leak into client code:
- Creation:
hire()builds the object and registers it in one step, so an object can’t exist without being tracked. - Lookup:
get()andlist_by_department()answer queries without exposing the underlying storage. - Mutation:
give_raise()modifies managed objects through the manager, which keeps invariants enforceable at a single choke point. - Cleanup:
fire()removes the object and its index entries — the order matters, because removing from the store first would leave the index pointing at nothing.
Two consequences here tend to bite people in production, so they’re worth a closer look.
The manager is an identity map. Two calls to get("a1b2c3d4") return the same instance, so a mutation made through one reference is visible through every other reference. That’s usually what you want — one canonical Employee per ID — but it also means aliasing bugs: code that mutates an object it fetched can surprise another caller holding the same instance.
Index consistency is the hard part. The example keeps a second map from department to employee IDs. Every write path — hire, fire, a hypothetical transfer — has to update both structures, and the manager is the only place where that discipline can be enforced. If the manager runs in a multi-threaded context, this is also where the locking belongs: ConcurrentHashMap in Java, a threading.Lock around the mutation methods in Python, or a single-writer convention. Centralizing concurrency control is arguably one of the pattern’s biggest payoffs, because each caller doing its own locking never works.
Variants
| Variant | Responsibility | Example |
|---|---|---|
| Entity Manager | CRUD + queries | JPA EntityManager, Django ORM Manager |
| Resource Manager | Pooling and reuse | Database connection pools, thread pools |
| Scene Manager | Lifecycle in games | Unity SceneManager, game object managers |
| Service Manager | Registry and lookup | Android ServiceManager, plugin registries |
| Cache Manager | Eviction and TTL | Redis cache wrappers, in-memory caches |
What Works
- Keep managers focused. One manager, one object type. A
UserManagerthat also sends emails and renders dashboards is a God class forming. - Return immutable views. Hand out copies or read-only wrappers of internal collections; a caller that can mutate
deptIndexcan silently corrupt it. - Update every index on every write. A stale index is worse than no index — it returns wrong answers confidently.
- Design the miss case deliberately. Decide whether
get()returnsOptional,null, or throws, and document it. Callers will build assumptions around whatever you choose. - Support batch operations when the domain calls for them.
hire_many()andfire_all_in_department()are common once the collection grows.
Common Mistakes
- The manager becomes a God class. If it manages unrelated objects, split it before the coupling hardens.
- Exposing internal state. Returning a modifiable reference to the internal map lets callers bypass every invariant you set up.
- Forgetting to clean up indexes. Removing from the store but not the index leaves phantom IDs that resurface in query results.
- Synchronous I/O inside manager methods. If
get()hits the network or disk, offload that work — managers in async contexts block whole event loops. - Over-managing trivial objects. For a handful of items used in one place, a plain list is clearer than a manager class.
Real-World Examples
EntityManager (JPA)
Jakarta Persistence’s EntityManager is the canonical manager: it persists, finds, merges, and removes entities while holding a first-level cache and tracking pending changes per transaction. The persistence specification documents how the entity lifecycle — new, managed, detached, removed — is exactly the lifecycle coordination this pattern describes.
Android ServiceManager
Android’s ServiceManager registers system services (window manager, package manager, sensors) by name and hands out binder references to callers. One central registry, one lookup contract, no direct instantiation by clients.
Scene and asset managers in game engines
Engines like Unity centralize scene lifecycle behind SceneManager, and most engines apply the same idea to sprites, physics bodies, and audio sources — each manager owns creation, per-frame updates, and destruction for its object type.
Django Managers
Django’s Managers sit between models and the database: Question.objects.filter(...) goes through the manager, which keeps query construction out of the model class itself.
Frequently Asked Questions
What is the difference between a Manager and a Repository?
A Repository abstracts persistence — it makes a database look like a collection. A Manager is broader: it coordinates lifecycle, enforces invariants, and may hold business rules about the collection. In practice they overlap, and many repositories are implemented as managers over a data mapper.
How is a Manager different from a dependency injection container?
A DI container manages services — usually singletons resolved by type at startup. A Manager typically manages domain objects created and destroyed at runtime. If your only need is "give me the one instance of X", dependency injection is the simpler tool.
Can a manager be a singleton?
Often, yes — a single source of truth for one resource type suits the Singleton Pattern well. Prefer injecting the manager over reaching for a global, though; it keeps tests honest and lets you scope managers per request or per tenant later.
How do I unit test code that uses a manager?
Put the manager behind an interface or a small protocol (hire, get, fire) and hand the unit under test a fake in-memory implementation. Testing the manager itself is straightforward too: no external dependencies in this design, so hire/fire/index assertions are pure unit tests.
Who owns thread safety — the manager or its callers?
The manager, always. Callers can't coordinate with each other, so per-caller locking only guards against that caller's own races. Put the lock inside the mutation methods, or use concurrent collections, and document the guarantee.
What should the manager do when an object isn't found?
Pick one contract and stick to it. Returning Optional/null works when missing objects are routine; throwing a NotFoundError suits cases where a missing ID signals a bug. Mixing the two across methods is worse than either choice alone.
Related Resources
Facade Pattern
Provide a simplified interface to a complex subsystem. A structural pattern that hides implementation details behind a clean API.
PatternSingleton Pattern: Java, Python & TypeScript Examples
Ensure a class has only one instance and provide global access to it — the creational design pattern for controlled object creation, with production-ready implementations.
PatternDependency Injection Pattern
Supply dependencies from outside rather than creating them internally. An architectural pattern for decoupled, testable code.
PatternBusiness Delegate Pattern
Reduce coupling between presentation and business tiers by introducing an intermediary that handles lookup, creation, and invocation of business services.
PatternContext Object Pattern: Examples
Reduce parameter bloat by passing a single context object through the call chain. Examples in Python, Java, and JavaScript for request contexts.
PatternRepository Pattern
Abstract data access logic behind a clean interface. An architectural design pattern for testable, maintainable data layers.