Photo by Bibhash (Polygon.Cafe) Banerjee on Unsplash

Domain Driven Design: Transaction scripts and active records

#[Pragmatic(Kiwi)]

--

When building software, not all problems require a sledgehammer. For straightforward tasks, simplicity and clarity are your best tools.

Today we will learn two patterns for managing simple business logic: Transaction Script and Active Record

Transaction Script

It organizes business logic as a sequence of steps, like following a recipe. Each operation is a self-contained script that either fully succeeds or fully fails, ensuring data consistency.

When to use it?

Simple operations: CRUD tasks
ETL Pipelines: Extract, Transform, Load
Supporting subdomains: Non-core parts of the system (logging, reporting, etc)

A good example would be the scenario in which you have a banking app where a user withdraws money. Two critical steps must happen atomically:

  • Deduct the amount from the account balance
  • Log the transaction in the audit table
def withdraw(account_id, amount):
try:
db.start_transaction()

acount = db.query("SELECT balance FROM accounts WHERE id = ?", account_id)

if account.balance < amount:
raise InsufficientFundsError()

db.execute("UPDATE accoutns SET balance = balance - ? where ID = ?", amount, account_id)

db.execute("INSERT INTO transactions (account_id, amount, type) VALUES (?, ?, 'withdrawn')", account_id, amount)

db.commit();
except:
db.rollback()
raise

Why this works?

  • Atomicity: The transaction ensures both steps succeed or neither does—no partial withdrawals.
  • Simplicity: Easy to read and debug — no hidden abstractions

Pitfalls:

  • Duplication: Repeated logic across scripts (e.g. balance checks)
  • Scalability: Hard to manage if business rules grow complex

Active Record — data and behavior in one package

The active record wraps a database row into an object that includes data and manipulation methods. It’s like a box that can organize its contents.

When to use it?

Data-centric operations: managing user profiles, product catalogs
Moderate complexity: When data structures are nested but business rules are simple

A good example is to imagine a user profile system where each user has an email, password, and profile data. The Active Record handles validation and persistence.

class User
attr_accessor :id, :email, :password_hash

def initialize(email, password)
@email = email
self.password = password
end

def password=(plaintext)
@password_hash = hash_password(plaintext)
end

def save
validate_email_format!
validate_password_strength!

db.execute("INSERT INTO users (email, password_hash) VALUES (?, ?)", @email, @password_hash)
end

private

def validate_email_format!
raise InvalidEmailError unless @email =~ /@/
end

def validate_password_strength!
raise WeakPasswordError if @password_hash.length < 8
end
end


user = User.new("alice@example.com", "s3cur3p@ss")
user.save

Why it works?

  • Encapsulation: Validation and persistence logic lives within the object
  • Reusability: Methods like save can be reused across the codebase

Pitfalls

  • Anemic models: If the object only has getter/setters and no behavior, it’s just a big data bag
  • Coupling: Tight integration with the database schema makes migration tricky

How do I choose the right tool for the job?

Here is a simple framework to help you with the

+-----------------+--------------------------------+----------------------------+
| Criteria | Transaction Script | Active Record |
+=================+================================+============================+
| Complexity | Simple, linear workflows | Nested data structure |
+-----------------+--------------------------------+----------------------------+
| Consistency | Requires explicit transactions | Built-in save/update logic |
+-----------------+--------------------------------+----------------------------+
| Maintainability | Prone to duplication | Centralizes data behavior |
+-----------------+--------------------------------+----------------------------+

Key takeaways and advice

Relax, if losing 0,001% of the data won’t break the system, avoid over-engineering.

Both patterns lack the rigor for complex business rules. Save them for supporting tasks.

The transaction script is your go-to for step-by-step operations. Think “scripts, not “systems”.

Active record shines when data and simple behavior are intertwined. Just don’t let it turn into a passive data holder.

--

--

No responses yet