In the previous post, I explained why I chose to build the Mohojo Werks backend myself. Building it also meant deciding how to organize the business logic. I wanted to keep my Rails controllers lean and DRY, with the business logic separate from HTTP handling. I chose a command-driven approach using my own Ruby gem, simple_command_dispatcher.

I’ll use Accumoo, our activity and progress tracking app, for the examples in this article. The following sections introduce the key components of the backend’s command-driven architecture, their responsibilities, and how they interact. Then I’ll trace an Event increment from request to response.

Controllers

Controllers handle HTTP requests and responses. Rails routes the request to a controller action. The controller’s responsibilities are to:

  • Filter request parameters.
  • Supply authenticated user and application context.
  • Dispatch the operation to a command.
  • Render the command’s result or translate its errors into an HTTP response.

Commands

Each command represents a cohesive application operation. Its responsibilities are to:

  • Enforce authorization rules.
  • Apply API-version-specific record visibility.
  • Validate incoming changes and operation-specific requirements.
  • Coordinate transactions and delegate to models and policy objects.
  • Invoke the appropriate formatter and return the operation’s result.

For example, an increment command coordinates lifecycle checks, period cycling, the count update, and history creation. These steps collectively implement incrementing an Event correctly. API-version-specific commands keep each supported contract independent while operating on shared models.

Models

Models enforce data invariants and manage persistence. Commands use Active Record models to query and save data through their associations. Model validations enforce constraints on attributes such as Event names, nonnegative counts, and Challenge metadata during normal saves. Command validation supplements these checks with operation-specific rules, such as requiring a positive increment amount.

Policy objects

Policy objects encapsulate reusable domain rules, reducing duplication across commands. A command supplies the relevant account or Event context and uses the policy’s decision to permit, reject, or constrain the operation. For example, EventAdmissionPolicy determines whether an Event can be admitted under the account’s limits; the command coordinates the write.

Response formatters

Response formatters separate persistence from serialization. Each API version’s formatter selects response fields and serializes attribute values according to its response contract, without modifying persisted data. Accumoo has one shared Rails Event model: the same Event record is serialized with whole-second timestamps in v1 and millisecond timestamps in v2. A command invokes its version’s formatter and returns the result to the controller for JSON rendering.

The simple_command_dispatcher gem

My gem, simple_command_dispatcher, provides command dispatch. I’ll call it SCD here. The controller supplies a command path through the Rails adapter described below; SCD resolves it to a Ruby class, invokes .call with the supplied parameters, and returns the command’s outcome. Its optional CommandCallable module provides result, errors, success?, and failure?.

I had never used SCD in production before Accumoo. This was a risk: integrating the gem into a live application tested assumptions that writing the gem alone hadn’t tested.

Why I chose it

SCD provided much of the infrastructure I needed for convention-based dispatch. Centralized dispatch reduces controller boilerplate, while command specs let me test operations directly. Command objects don’t require SCD; I chose it to standardize their invocation.

The component boundaries also define what I test: command behavior, policy decisions, response contracts, and request-to-command integration.

The integration I had to add

SCD resolves a command path, but the application must construct that path from a Rails request. A resource URL contains an ID, and its operation depends on the matched action: GET /events/:uuid means Show, while PATCH to the same resource means Update.

I added extract_command_namespace to Accumoo::Api::BaseController. It reads the matched route pattern, removes dynamic segments and the optional format suffix, preserves the API version, and appends the action when necessary. Api::BaseCommandController supplies action declaration helpers, while the Commandable concern handles shared dispatch and response rendering alongside authentication.

IMPORTANT: Command resolution uses the matched Rails route and action. Clients supply operation parameters, not an arbitrary command class.

I want to extract the reusable Rails route-to-command adapter into SCD. Authentication and application-specific authorization remain in the backend. The adapter currently depends on Rails routing internals, so extraction needs compatibility tests rather than a straight copy into the gem.

Putting it all together

Accumoo’s mobile client saves an increment locally and updates its UI after the SQLite transaction commits. Later, when connected, it pushes pending changes through the API. The diagram shows this synchronization request and the HTTP response returning to the client.

The mobile client saves locally and updates its UI before synchronizing. A later sync push becomes an HTTP request, passes through the controller, Rails adapter and SCD to the Push command, and returns an HTTP response to the client. Compact boxes identify MW backend code and the SCD gem.

Open full-size diagram: local save, command dispatch, and response

The same dispatch mechanism handles backend-driven operations such as POST /accumoo/api/v2/events/:uuid/increment. The excerpts below use this smaller operation to illustrate the code; mobile clients synchronize their locally saved changes rather than calling this increment endpoint.

These excerpts are abbreviated from the implementation. Namespaces use qualified class names for brevity; # ... marks omitted code, including authentication, parameter filtering, helper implementations, and error branches.

The controller’s base class dispatches to Accumoo::Api::V2::Events::Increment:

class Accumoo::Api::BaseController < ::Api::BaseCommandController
  # ...

  private

  def dispatch_command
    command = SimpleCommandDispatcher.call(
      command: extract_command_namespace,
      request_params: sanitized_params,
      options: command_options
    )

    render_command_result(command)
    # ... rescue clauses omitted
  end

  # ...
end

The command validates the request, performs the increment, and serializes the Event. Inside increment_event, the related writes share a transaction after the scoped lookup and state-machine check:

class Accumoo::Api::V2::Events::Increment
  prepend SimpleCommandDispatcher::Commands::CommandCallable
  # ... includes, initialization, and helper methods omitted

  def call
    validate_params
    return if errors.any?

    event = increment_event
    return unless event

    { event: format_event(event) }
  end

  private

  def increment_event
    event = api_events.find(uuid)
    # ... lifecycle eligibility check omitted

    ActiveRecord::Base.transaction do
      cycle_period_if_needed(event, user_app: accumoo_user_application)
      event.count += amount
      event.save!
      event.create_increment_history_entry!
    end

    event
  end

  # ...
end

SCD’s CommandCallable stores the returned hash in command.result. The controller’s response handler renders this as JSON on success, or maps command errors to the appropriate HTTP response.

Benefits, costs, and improvements

The benefits are lean controllers, cohesive application operations, and focused command specs. Convention-based dispatch reduces controller boilerplate, while version-owned commands and formatters keep API contracts explicit.

The cost is more classes, dispatch conventions, and integration code to maintain. Dynamic command resolution also means a missing or misnamed command fails at runtime.

There are also SRP and DRY violations to address. Create and update commands duplicate premium-feature entitlement policy; I want to extract this into a policy object. The EventFormatter concerns also calculate tracking-period dates for writes, mixing serialization with domain calculations. These responsibilities should be separated while preserving each API version’s contract.

Commands also aren’t the only possible write path. Domain invariants need enforcement across background jobs and maintenance scripts, with database constraints where appropriate. Moving logic out of controllers doesn’t remove this responsibility.

The architecture is useful, and it has concrete improvements to make. You can explore SCD’s source and my other projects on GitHub.