Active Object Pattern
Solution: Active Object with Command Queue
Read aloud is unavailable in this browser.
A Principled Approach to Concurrency
The Active Object pattern is one of the most elegant solutions in the concurrency patterns catalog. It transforms the complex problem of thread-safe asynchronous execution into a clean, structured architecture where method calls are converted into command objects, queued for processing, and executed by a dedicated scheduler thread.
The genius of this pattern lies in its inversion: instead of scattering synchronization logic throughout your codebase, Active Object centralizes all synchronization in the scheduler. Clients interact with a simple, familiar object interface—unaware that behind the scenes, their calls are being serialized through a message queue.
This pattern was formally described by Douglas C. Schmidt and has been widely adopted in systems ranging from real-time embedded software to CORBA middleware to modern game engines.
By the end of this page, you will understand the complete architecture of the Active Object pattern—every component, how they interact, and why each is necessary. You'll see how method invocation flows through the pattern and how results are delivered back to callers.
The Pattern's Core Insight
The Active Object pattern is built on a profound insight:
Method invocation and method execution are fundamentally different concerns that can—and often should—be separated.
In traditional OOP, a method call immediately triggers execution on the caller's thread. Active Object breaks this assumption by introducing an indirection layer:
- Method Call → Creates a Method Request (command object)
- Method Request → Enqueued in an Activation Queue
- Scheduler → Dequeues and dispatches to the Servant (actual implementation)
- Result → Delivered via a Future/Promise to the original caller
This separation transforms a synchronous, blocking call into an asynchronous, non-blocking operation while maintaining a familiar method-call interface.
The caller's thread never blocks waiting for execution. It receives a Future immediately and can continue working. The actual execution happens later, on the scheduler's thread—completely decoupled from the invocation timeline.
The Six Architectural Components
The Active Object pattern consists of six precisely defined components, each with a specific responsibility:
| Component | Responsibility | Thread Context |
|---|---|---|
| Proxy | Exposes the same interface as the Servant; converts method calls into Method Request objects and enqueues them | Runs on client thread |
| Method Request | Encapsulates a method invocation as an object (command pattern); contains method identity, parameters, and associated Future | Created on client thread, executed on scheduler thread |
| Activation Queue | Thread-safe queue holding pending Method Requests; decouples producers (proxy) from consumer (scheduler) | Accessed by multiple threads (synchronized) |
| Scheduler | Runs in dedicated thread(s); dequeues Method Requests and dispatches them to the Servant for execution | Dedicated scheduler thread(s) |
| Servant | Contains the actual business logic implementation; processes method requests when dispatched by the scheduler | Runs on scheduler thread |
| Future | Placeholder for the eventual result; allows client to check completion status or block until result is available | Written by scheduler thread, read by client thread |
Let's examine each component in detail, understanding not just what it does but why it's designed this way.
Component Deep Dive: The Proxy
The Proxy is the client's gateway to the Active Object. It exposes exactly the same interface as the real implementation (the Servant), making the async nature transparent to callers. However, instead of executing methods directly, the Proxy:
- Creates a Method Request object capturing the method identity and parameters
- Creates a Future to hold the eventual result
- Associates the Future with the Method Request
- Enqueues the Method Request in the Activation Queue
- Returns the Future immediately to the caller
This happens instantly—the caller is never blocked.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
/** * The Proxy component of Active Object pattern * Exposes same interface as Servant but returns Futures instead of direct results */ // The interface shared by Proxy and Servantinterface ImageProcessor { processImage(imageId: string, data: Buffer): Promise<ProcessedImage>; resizeImage(imageId: string, width: number, height: number): Promise<ProcessedImage>; applyFilter(imageId: string, filterName: string): Promise<ProcessedImage>;} /** * Proxy: Converts synchronous-looking calls into asynchronous Method Requests */class ImageProcessorProxy implements ImageProcessor { private readonly activationQueue: ActivationQueue; constructor(activationQueue: ActivationQueue) { this.activationQueue = activationQueue; } processImage(imageId: string, data: Buffer): Promise<ProcessedImage> { // Step 1: Create a Future for the eventual result const future = new Future<ProcessedImage>(); // Step 2: Create a Method Request encapsulating the call const methodRequest = new ProcessImageRequest( imageId, data, future ); // Step 3: Enqueue the request (non-blocking) this.activationQueue.enqueue(methodRequest); // Step 4: Return immediately with the Future // The client now has a handle to the eventual result return future.toPromise(); } resizeImage(imageId: string, width: number, height: number): Promise<ProcessedImage> { const future = new Future<ProcessedImage>(); const methodRequest = new ResizeImageRequest(imageId, width, height, future); this.activationQueue.enqueue(methodRequest); return future.toPromise(); } applyFilter(imageId: string, filterName: string): Promise<ProcessedImage> { const future = new Future<ProcessedImage>(); const methodRequest = new ApplyFilterRequest(imageId, filterName, future); this.activationQueue.enqueue(methodRequest); return future.toPromise(); }} // Client code looks almost synchronous!async function clientExample(processor: ImageProcessor) { console.log("Starting image processing..."); // This returns immediately—the call is queued, not executed const resultPromise = processor.processImage("img-123", imageBuffer); console.log("Processing queued, doing other work..."); // ... do other work while processing happens in background ... // When we need the result, we await it const processed = await resultPromise; console.log("Processing complete:", processed);}Note that the Proxy implements the same interface as the Servant would. The only difference is that return types are wrapped in Futures. This consistency is key—clients can work with Active Objects using familiar patterns, and dependency injection can swap implementations transparently.
Component Deep Dive: Method Request
The Method Request is a concrete application of the Command pattern. It encapsulates everything needed to execute a method at a later time:
- Method identity — Which method to call
- Parameters — The arguments for that method
- Future reference — Where to deliver the result (or error)
Each method on the interface gets a corresponding Method Request class. This reification of method calls as objects is what enables the temporal decoupling at the heart of Active Object.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
/** * Method Request: Encapsulates a method call as an object (Command Pattern) */ // Abstract base for all method requestsabstract class MethodRequest<T> { protected readonly future: Future<T>; constructor(future: Future<T>) { this.future = future; } /** * Called by the Scheduler to execute the actual method on the Servant. * Subclasses implement the specific method invocation. */ abstract call(servant: ImageProcessorServant): void; /** * Optional: Guard condition for execution. * Can be used to implement conditional synchronization. */ guard(servant: ImageProcessorServant): boolean { return true; // Default: always executable }} /** * Concrete Method Request for processImage */class ProcessImageRequest extends MethodRequest<ProcessedImage> { private readonly imageId: string; private readonly data: Buffer; constructor(imageId: string, data: Buffer, future: Future<ProcessedImage>) { super(future); this.imageId = imageId; this.data = data; } call(servant: ImageProcessorServant): void { try { // Execute the actual method on the Servant const result = servant.processImage(this.imageId, this.data); // Deliver success to the waiting Future this.future.resolve(result); } catch (error) { // Deliver failure to the waiting Future this.future.reject(error as Error); } }} /** * Concrete Method Request for resizeImage */class ResizeImageRequest extends MethodRequest<ProcessedImage> { private readonly imageId: string; private readonly width: number; private readonly height: number; constructor( imageId: string, width: number, height: number, future: Future<ProcessedImage> ) { super(future); this.imageId = imageId; this.width = width; this.height = height; } call(servant: ImageProcessorServant): void { try { const result = servant.resizeImage(this.imageId, this.width, this.height); this.future.resolve(result); } catch (error) { this.future.reject(error as Error); } }} /** * Concrete Method Request for applyFilter */class ApplyFilterRequest extends MethodRequest<ProcessedImage> { private readonly imageId: string; private readonly filterName: string; constructor(imageId: string, filterName: string, future: Future<ProcessedImage>) { super(future); this.imageId = imageId; this.filterName = filterName; } call(servant: ImageProcessorServant): void { try { const result = servant.applyFilter(this.imageId, this.filterName); this.future.resolve(result); } catch (error) { this.future.reject(error as Error); } } // Example of a guard: only apply filter if image exists in cache guard(servant: ImageProcessorServant): boolean { return servant.hasImage(this.imageId); }}The optional guard() method enables sophisticated conditional synchronization. A Method Request's guard can inspect the Servant's state to determine if execution should proceed. This is useful for implementing producer-consumer patterns or waiting for specific conditions within the Active Object.
Component Deep Dive: Activation Queue
The Activation Queue is the critical synchronization point in the Active Object pattern. It's a thread-safe, typically bounded queue that:
- Accepts Method Requests from the Proxy (producer side)
- Supplies Method Requests to the Scheduler (consumer side)
- Provides ordering guarantees (usually FIFO)
- Handles backpressure when the queue is full
This is the only shared state between the client threads and the scheduler thread, and it's encapsulated in a well-tested concurrent data structure.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
/** * Activation Queue: Thread-safe queue connecting producers (Proxy) to consumer (Scheduler) */ class ActivationQueue { private readonly queue: Array<MethodRequest<unknown>> = []; private readonly maxSize: number; private readonly lock = new Mutex(); private readonly notEmpty = new ConditionVariable(); private readonly notFull = new ConditionVariable(); private isShutdown = false; constructor(maxSize: number = 1000) { this.maxSize = maxSize; } /** * Enqueue a method request (called by Proxy on client thread) * Blocks if queue is full (backpressure) */ async enqueue(request: MethodRequest<unknown>): Promise<void> { await this.lock.acquire(); try { // Wait if queue is full and not shutdown while (this.queue.length >= this.maxSize && !this.isShutdown) { await this.notFull.wait(this.lock); } if (this.isShutdown) { throw new Error("Queue is shutdown"); } // Add request and signal scheduler this.queue.push(request); this.notEmpty.signal(); } finally { this.lock.release(); } } /** * Dequeue a method request (called by Scheduler on its thread) * Blocks if queue is empty */ async dequeue(): Promise<MethodRequest<unknown> | null> { await this.lock.acquire(); try { // Wait if queue is empty and not shutdown while (this.queue.length === 0 && !this.isShutdown) { await this.notEmpty.wait(this.lock); } if (this.queue.length === 0) { return null; // Shutdown with empty queue } const request = this.queue.shift()!; this.notFull.signal(); return request; } finally { this.lock.release(); } } /** * Shutdown the queue gracefully */ async shutdown(): Promise<void> { await this.lock.acquire(); try { this.isShutdown = true; // Wake up all waiting threads this.notEmpty.signalAll(); this.notFull.signalAll(); } finally { this.lock.release(); } } /** * Get current queue depth (for monitoring) */ async size(): Promise<number> { await this.lock.acquire(); try { return this.queue.length; } finally { this.lock.release(); } }}A bounded queue provides natural backpressure. When producers create work faster than the scheduler can process it, the queue fills up. Further enqueue attempts block, slowing down producers. This prevents memory exhaustion and creates a sustainable balance between production and consumption rates.
Component Deep Dive: The Scheduler
The Scheduler is the heart of the Active Object. It runs on a dedicated thread (or thread pool), continuously:
- Dequeuing Method Requests from the Activation Queue
- Checking guard conditions (if present)
- Dispatching executable requests to the Servant
- Handling the results and populating Futures
The Scheduler's thread is the only thread that ever touches the Servant, which means the Servant never needs its own synchronization. This is the key insight that eliminates lock complexity from business logic.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
/** * Scheduler: The execution engine of the Active Object * Runs in a dedicated thread, processes Method Requests serially */ class Scheduler { private readonly activationQueue: ActivationQueue; private readonly servant: ImageProcessorServant; private isRunning = false; private workerPromise: Promise<void> | null = null; constructor(activationQueue: ActivationQueue, servant: ImageProcessorServant) { this.activationQueue = activationQueue; this.servant = servant; } /** * Start the scheduler's processing loop */ start(): void { if (this.isRunning) { throw new Error("Scheduler is already running"); } this.isRunning = true; this.workerPromise = this.processLoop(); console.log("[Scheduler] Started"); } /** * Main processing loop - runs continuously on dedicated thread */ private async processLoop(): Promise<void> { while (this.isRunning) { try { // Block until a request is available const request = await this.activationQueue.dequeue(); if (request === null) { // Queue was shutdown break; } // Check guard condition before executing if (request.guard(this.servant)) { // Dispatch to servant - this runs the actual business logic // The request's call() method handles try/catch and Future completion request.call(this.servant); } else { // Guard failed - what to do depends on requirements // Option 1: Re-queue for later (careful of infinite loops) // Option 2: Fail the future with appropriate error // Option 3: Put in a separate "waiting" queue console.log("[Scheduler] Guard condition not met, re-queuing..."); await this.activationQueue.enqueue(request); // Small delay to prevent busy-wait if guard keeps failing await this.delay(10); } } catch (error) { console.error("[Scheduler] Error processing request:", error); // Continue processing other requests despite errors } } console.log("[Scheduler] Stopped"); } /** * Stop the scheduler gracefully */ async stop(): Promise<void> { this.isRunning = false; await this.activationQueue.shutdown(); if (this.workerPromise) { await this.workerPromise; } console.log("[Scheduler] Shutdown complete"); } private delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }}Because the Scheduler is the only component that calls the Servant, and the Scheduler runs in a single dedicated thread, the Servant is effectively single-threaded. This means you can write the Servant's business logic without any synchronization—just normal, sequential code. The Active Object pattern has externalized all concurrency concerns.
Component Deep Dive: Servant and Future
The Servant contains the actual business logic implementation. It's where the real work happens. Because it's only ever accessed by the Scheduler's single thread, it can be written as simple, synchronous code with no thread-safety concerns.
The Future is the result delivery mechanism. It's created by the Proxy, passed along with the Method Request, and completed by the Method Request after execution. The client can:
- Check if the result is ready (non-blocking)
- Block until the result is available
- Register callbacks to be notified upon completion
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
/** * Servant: The actual implementation * Just regular, synchronous code! */ class ImageProcessorServant { // State can be mutable - only // one thread accesses it private cache = new Map<string, Image>(); processImage( imageId: string, data: Buffer ): ProcessedImage { // Plain synchronous code const decoded = this.decode(data); const filtered = this.filter(decoded); const resized = this.resize(filtered); // Update state freely this.cache.set(imageId, resized); return this.compress(resized); } resizeImage( imageId: string, w: number, h: number ): ProcessedImage { const image = this.cache.get(imageId); if (!image) { throw new Error("Image not found"); } return this.resize(image, w, h); } applyFilter( imageId: string, filter: string ): ProcessedImage { const image = this.cache.get(imageId); return this.applyFilterImpl( image!, filter ); } hasImage(imageId: string): boolean { return this.cache.has(imageId); } // Private implementation methods... private decode(data: Buffer): Image { /* ... */ } private filter(img: Image): Image { /* ... */ } private resize( img: Image, w?: number, h?: number ): Image { /* ... */ } private compress(img: Image): ProcessedImage { /* ... */ }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
/** * Future: Placeholder for eventual result */ class Future<T> { private result: T | undefined; private error: Error | undefined; private isCompleted = false; private callbacks: Array<{ onSuccess: (v: T) => void; onError: (e: Error) => void; }> = []; /** * Set successful result * (called by Method Request) */ resolve(value: T): void { if (this.isCompleted) return; this.result = value; this.isCompleted = true; this.notifyCallbacks(); } /** * Set failure * (called by Method Request) */ reject(error: Error): void { if (this.isCompleted) return; this.error = error; this.isCompleted = true; this.notifyCallbacks(); } /** * Convert to Promise for * async/await usage */ toPromise(): Promise<T> { return new Promise((resolve, reject) => { if (this.isCompleted) { if (this.error) { reject(this.error); } else { resolve(this.result!); } } else { this.callbacks.push({ onSuccess: resolve, onError: reject, }); } }); } /** * Check if result is ready */ isDone(): boolean { return this.isCompleted; } private notifyCallbacks(): void { for (const cb of this.callbacks) { if (this.error) { cb.onError(this.error); } else { cb.onSuccess(this.result!); } } this.callbacks = []; }}In practice, you rarely need to implement Future yourself. Most languages provide built-in or standard library implementations: • Java: CompletableFuture, Future • TypeScript/JavaScript: Promise • C++: std::future • Python: asyncio.Future, concurrent.futures • C#: Task<T>
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.