Command Bus Pattern

In 2013, at EVALS, I implemented the command bus pattern for the first time quite heavily. We had a modular backend, and using it as a way to coordinate across module boundaries felt cleaner than injecting services or passing DTOs around. It had intent baked in. Action. For the right use cases, it worked beautifully, so we used it heavily.

Here's where people trip up: a command bus isn't supposed to return anything meaningful. That's not a limitation. That's the point. You're issuing a command, not asking a question. "Do this." Not "Do this and tell me what happened."

This creates a problem in people's heads when they're creating new entities. "But I need the ID back." No you don't. You need to stop using auto-increment IDs like it's 2005.

If you're generating UUIDs (and you should be), the responsibility of defining that identifier doesn't belong to the database or the entity. It belongs to whoever is issuing the command. Generate the UUID before you dispatch. Now you already know it. Pass it in. The command handler uses it. You move on with your life.

The command bus shines when you want to decouple intent from execution. Module A doesn't need to know how Module B creates a user. It just needs to say "create this user with this ID" and trust that it happens. If you need to query state afterward, that's a separate concern. Probably a separate pattern.

If you're reaching for a command bus and immediately asking "but how do I get the result back," you're either using it wrong or you don't actually want a command bus. You want a service. That's fine. Use the right tool.

// types.ts

interface Command {
  readonly kind: string;
}

type CommandHandler<T extends Command> = (command: T) => Promise<void>;


// command-bus.ts

class CommandBus {
  private handlers = new Map<string, CommandHandler<any>>();

  register<T extends Command>(kind: string, handler: CommandHandler<T>): void {
    this.handlers.set(kind, handler);
  }

  async dispatch<T extends Command>(command: T): Promise<void> {
    const handler = this.handlers.get(command.kind);
    if (!handler) {
      throw new Error(`No handler registered for ${command.kind}`);
    }
    await handler(command);
  }
}


// create-user-command.ts

interface CreateUserCommand extends Command {
  readonly kind: "CreateUser";
  readonly id: string;
  readonly email: string;
  readonly name: string;
}

function createUserCommand(id: string, email: string, name: string): CreateUserCommand {
  return { kind: "CreateUser", id, email, name };
}


// create-user-handler.ts

const handleCreateUser: CommandHandler<CreateUserCommand> = async (command) => {
  // This is where you'd persist to your database
  // The ID is already defined. You're not asking for anything back.
  console.log(`Creating user ${command.id}: ${command.name} (${command.email})`);
  
 await userRepository.save({ id: command.id, email: command.email, name: command.name });
};


// main.ts

import { randomUUID } from "crypto";

const bus = new CommandBus();
bus.register("CreateUser", handleCreateUser);

// The caller generates the ID. Not the database. Not the handler.
// You already know the ID before you dispatch. That's the whole point.
const userId = randomUUID();

await bus.dispatch(createUserCommand(userId, "[email protected]", "Jane Doe"));

// You need to query the user later? Different concern. Different pattern.

❤️
Jake