Design Patterns

Active Object Design Pattern

I had a dashboard that fired off five API calls on load. They all competed for the main thread. The UI froze, users complained, and my "loading spinner" w...

23 Mar 2024

Active Object Design Pattern

I had a dashboard that fired off five API calls on load. They all competed for the main thread. The UI froze, users complained, and my "loading spinner" was itself frozen. The fix was queuing those calls so they ran one at a time, in the background.

That's the Active Object pattern. It separates calling a method from executing it. You drop tasks into a queue. A scheduler picks them up and runs them asynchronously.

Think of it like a restaurant kitchen. Waiters (callers) drop order tickets on the rail. The chef (scheduler) picks them up one by one. The waiter doesn't stand there waiting — they go serve another table.

Javascript
class TaskRunner {
  constructor() {
    this.taskQueue = [];
    this.active = false;
  }

  enqueueTask(task) {
    this.taskQueue.push(task);
    if (!this.active) {
      this.executeNextTask();
    }
  }

  executeNextTask() {
    if (this.taskQueue.length > 0) {
      this.active = true;
      const task = this.taskQueue.shift();
      task().then(() => {
        this.active = false;
        this.executeNextTask();
      });
    }
  }
}

function sampleTask() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log("Task executed");
      resolve();
    }, 1000);
  });
}

const taskRunner = new TaskRunner();
taskRunner.enqueueTask(sampleTask);
taskRunner.enqueueTask(sampleTask);

TaskRunner is the active object. It owns a queue and processes tasks sequentially. Each task returns a promise. When one finishes, the next one starts.

This is a simplified version. In production you'd add error handling, concurrency limits, and maybe priority ordering.

The benefit: Non-blocking execution. The caller never waits. You control concurrency, ordering, and can add retry logic at the queue level.

The cost: Debugging is harder — the call site and execution site are decoupled. Errors surface in the queue, not where the task was enqueued. You also lose immediate return values; everything becomes async.

I use this pattern in event handling pipelines, rate-limited API callers, and background job processors. If your tasks can run independently and order matters, Active Object is a clean fit.

Keep reading