Tanaka Soft

Should Business Logic Live in the Model or Be Split into a UseCase?

A comparison of Rich ActiveRecord Models and UseCase-centered design, starting from business entities, business verbs, and state changes in Rails.

Design Trade-offs with Rails + AASM

When building a business system with Rails, you eventually encounter a familiar question.

Suppose there is a process that completes a sale transaction.

  • Change the transaction state from fulfilling to completed
  • Update the fulfillment record
  • Change the payment state
  • Record that the transaction was completed
  • Send an email if necessary

How much of this should be written in the Model?

Should calling

trade.complete!

complete the business process?

Or should you create a UseCase such as

CompleteTrade.call(trade_id)

and collect the transaction and associated-record updates there?

The answer is not determined solely by whether you use AASM. AASM is a component that adds a state machine to Ruby classes or ActiveRecord Models; it is not an architecture by itself. AASM README

What matters is where you place business verbs, state transitions, transactions, and external side effects.

Express the Business Before the Implementation

Using a sale transaction as an example, the business can be expressed as follows.

ElementDescription
Business unitSale transaction
Stateslisted, reserved, paid, fulfilling, completed
Business verbsList, purchase, pay, start fulfillment, complete
InvariantA completed transaction cannot be cancelled
Associated updatesPayment, fulfillment, completion record
External side effectsEmail, external payment, search-index update
flowchart LR
    A[Transaction] --> B[Business verbs]
    A --> C[State]
    B --> D[Associated record updates]
    B --> E[Email and external side effects]

The difference between the approaches comes from where these business elements are placed.

1. Rails MVC + Rich ActiveRecord Model

The first approach is to gather the state and business verbs in the ActiveRecord Model that represents the business unit.

class Trade < ApplicationRecord
  include AASM

  aasm column: :status do
    state :listed, initial: true
    state :reserved
    state :paid
    state :fulfilling
    state :completed

    event :reserve do
      transitions from: :listed, to: :reserved,
                  guard: :purchasable?
    end

    event :complete, after: :record_completion do
      transitions from: :fulfilling, to: :completed
    end
  end

  def record_completion
    fulfillment_record.complete!
    completion_record.create!
  end
end

The Controller calls the business verb.

def complete
  @trade.complete!
  TradeCompletedMailer.notify(@trade).deliver_later

  redirect_to @trade
end

In this structure, the Model knows:

  • Which states a transaction has
  • Which state transitions are allowed
  • The business verb of completing a transaction
  • Which associated records must always be updated on completion

With AASM’s ActiveRecord integration, state transitions and persistence can be handled as a transaction. If a transition callback or state update fails, related database changes can be rolled back. AASM Transaction Support

The important point is that updating multiple tables does not automatically mean the logic must not live in the Model.

If the updates move several associated records into a consistent state in order to complete one transaction—the single business unit they belong to—they can be read as a business rule belonging to the Trade Model.

Strengths

  • Business knowledge and the business unit stay together
  • States and business verbs can be read in one place
  • trade.complete! is a natural expression
  • A sequence of changes involving associated records can be handled together
  • Controllers stay thin
  • You do not need to open many files to follow the business process

Weaknesses

  • The Model can grow large
  • The team needs rules for adding processing to AASM callbacks
  • Putting email or external APIs in the Model expands its responsibility
  • Side effects that happen outside the Model become harder to trace
  • If the process is called from a non-web entry point, you must decide how to handle side effects such as email

This approach prioritizes cohesion of business knowledge around the business unit.

2. Rails MVC + DDD-oriented Domain Model / UseCase

Another approach is to separate the business model from the UseCase that represents the whole process. You keep Rails Controllers and Views while extracting part of the business process into a Domain Model or UseCase.

flowchart LR
    C[Controller] --> U[CompleteTrade UseCase]
    U --> R[Repository / ActiveRecord]
    R --> D[Domain Model]
    U --> T[DB transaction]
    U --> O[Email / Outbox / Job]

The Domain Model represents the business unit of the transaction.

class Trade
  include AASM

  attr_reader :id
  attr_accessor :status

  def initialize(id:, status:)
    @id = id
    @status = status.to_sym
  end

  aasm column: :status do
    state :fulfilling
    state :completed

    event :complete do
      transitions from: :fulfilling, to: :completed
    end
  end
end

The UseCase loads the Domain Model, executes the business verb, and persists the result. The following code simplifies the Repository and transaction implementations.

class CompleteTrade
  def call(trade_id)
    TradeRecord.transaction do
      record = trade_repository.find(trade_id)
      trade = record.to_domain

      trade.complete
      trade_repository.save(trade)
      outbox_repository.append!(
        event_type: "trade_completed",
        trade_id: trade.id
      )
    end
  end
end

In this structure, the Domain Model owns states and invariants, while the UseCase owns the overall process, including persistence.

  • The transaction boundary
  • Loading data from the Repository
  • Operating on the Domain Model
  • Coordinating multiple business objects
  • Recording an Outbox event or Job
  • Creating an event to pass to an external process

A thin UseCase is not automatically a bad thing. If it makes the transaction boundary or the application-level entry point explicit, it has a role even when it contains little code.

However, if a UseCase only calls one Model method and owns neither a transaction nor an external side effect, a Model-centered design may be simpler.

Strengths

  • Transaction boundaries are explicit
  • Processes involving multiple business objects are easier to express
  • Side effects such as email and external APIs are easier to separate
  • The same process is easier to call from non-Controller entry points
  • The overall sequence is readable in the UseCase

Weaknesses

  • You need to read multiple files such as the Model, UseCase, and Repository
  • The location of the business verb becomes harder to find
  • UseCases tend to become thin
  • You need rules for deciding whether something belongs in the Model or UseCase
  • The cognitive load of following one business process increases

Being DDD-oriented inside Rails MVC does not necessarily mean eliminating Rails or ActiveRecord. You can use a Rails Model as an Aggregate while extracting UseCases or Domain Models only where it helps.

3. A Hybrid Form Using Both Model and UseCase

In practice, a design may use both.

For example, you could define rules like these.

ProcessLocation
State definitionsModel + AASM
State-transition conditionsModel + AASM
Updates inside the AggregateModel
Coordination across multiple business unitsUseCase
Transaction boundaryUseCase
Email and external APIsUseCase or Job

This approach may provide the strengths of both, but it is not free.

Every time, the implementer has to decide:

  • Is this a state transition?
  • Is this a business rule that accompanies a state transition?
  • Is this an update inside the Aggregate?
  • Is this coordination across multiple business units?
  • Is this the transaction boundary?
  • Is this an external side effect?
flowchart TD
    A[Write the process] --> B{What kind of process is it?}
    B -->|State transition| C[Model + AASM]
    B -->|Update inside Aggregate| D[Model]
    B -->|Coordinate multiple units| E[UseCase]
    B -->|Email or external API| F[UseCase / Job]

If everyone on the team understands these rules, the split can be clean. But compared with a simple Model-centered design, the cognitive load is higher because there are more rules to remember.

The hybrid form is not “the correct answer that gives you both benefits.”

It is a design that accepts the rules and cognitive load required to gain both Model cohesion and explicit UseCase boundaries.

The Choice Depends on What You Prioritize

PrioritySuitable structure
Gather business knowledge around the business unitRich ActiveRecord Model
Read states and business verbs in the same placeRich ActiveRecord Model
Avoid following a process across multiple filesRich ActiveRecord Model
Make the transaction boundary explicitUseCase-centered
Manage external side effectsUseCase-centered
Call the same process from multiple entry pointsUseCase-centered
Separate the Model and process sequence in detailModel + UseCase
Accept more placement rulesModel + UseCase

The important thing is not to decide “Model or UseCase?” first.

  1. What is the business unit?
  2. Which business verbs belong to that unit?
  3. Which states and invariants must be protected together?
  4. Which database changes should form one atomic unit?
  5. Where do external side effects begin?

If you ask these questions in this order, a design that puts AASM in a Rails Model and a design that separates work into a Domain Model or UseCase can both be explained from the same business structure.

Summary

Rails MVC can gain cohesion and locality by keeping the business unit and its business verbs close to the Model. AASM can express the states and permitted business verbs of that unit.

When you split the design in a DDD-oriented way, you separate the business model from persistence and process coordination. Both approaches can be implemented inside Rails MVC; the difference is how far you separate business logic from persistence and processing order.

A design that uses both approaches can gain benefits from each, but it also adds rules for deciding where each piece belongs.

Ultimately, choosing a design means deciding what to prioritize.

  • If you prioritize cohesion of business knowledge, choose a Model-centered design.
  • If you prioritize explicit transaction and process boundaries, choose a UseCase-centered design.
  • If you prioritize both, use both while accepting the placement rules and cognitive load.

The important question is not which design is universally correct.

What do you want to keep in one place? Which boundaries do you want to make explicit? How many rules are you willing to remember to get there?

Once those choices are clear, you can select a Model-centered, UseCase-centered, or hybrid design.

References