StackPractices
beginner By Mathias Paulenko

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() and list_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.
flowchart diagram: Client[Client code

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

VariantResponsibilityExample
Entity ManagerCRUD + queriesJPA EntityManager, Django ORM Manager
Resource ManagerPooling and reuseDatabase connection pools, thread pools
Scene ManagerLifecycle in gamesUnity SceneManager, game object managers
Service ManagerRegistry and lookupAndroid ServiceManager, plugin registries
Cache ManagerEviction and TTLRedis cache wrappers, in-memory caches

What Works

  • Keep managers focused. One manager, one object type. A UserManager that 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 deptIndex can 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() returns Optional, 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() and fire_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.