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 An activity you want to track, such as reading, walking, or staying hydrated. 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 The interval an Event records progress for, such as the current day or week. Reset Period sets how often a new interval starts; Lifetime keeps a running total. cycling, the count update, and history A read-only view of recorded counts and past periods. Open an activity's menu to find it. 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 A time-limited goal with a clear finish line. Choose a difficulty level and record progress toward completing it. 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 Sync (synchronization) sends changes from your device to Mohojo Werks’ servers and brings those changes to your other devices when they sync. This keeps your Accumoo data up to date across devices. request and the HTTP response returning to the client.
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.