🧵 Series: Transactions with Active Record + PostgreSQL

📌 Post 2 – Nested Transactions in Rails: What Really Happens and When (Not) to Use Them

🧵 Series: Transactions with Active Record + PostgreSQL 📌 Post 2 – Nested Transactions in Rails: What Really Happens and When (Not) to Use Them

In the first post of this series, we saw how database transactions ensure atomicityconsistencyisolation, and durability(ACID). But what happens when you nest one transaction inside another?

Most developers assume that if something fails inside a nested `.transaction` block, the entire operation rolls back.

🚨 Spoiler: That's not exactly what happens.

In this post, you'll learn:

  • What nested transactions in Rails actually do
  • How PostgreSQL handles (or doesn't handle) nested transactions
  • What Rails simulates behind the scenes
  • A real-world case where nested transactions can help
  • Pitfalls, best practices, and warnings


💡 What Are Nested Transactions?

Nested transactions occur when you wrap a `.transaction` block inside another `.transaction`.

ActiveRecord::Base.transaction do
  # Outer transaction
  ActiveRecord::Base.transaction do
    # Inner transaction
  end
end        

You might expect that if the inner block fails, the whole transaction is rolled back. But that’s not exactly true in Rails.


🧠 What Actually Happens in Rails (and PostgreSQL)

🔍 PostgreSQL does not support true nested transactions.

So how does Rails manage it?

👉 Rails fakes it using SAVEPOINTs.

  • Each inner `.transaction` creates a savepoint.
  • If an error happens, Rails issues a `ROLLBACK TO SAVEPOINT`, undoing only the inner part.
  • The outer transaction stays open unless you manually raise an error.

This can lead to false assumptions of safety if you're not careful.


🛠 Practical Example: Inner Rollback, Outer Continues

ActiveRecord::Base.transaction do
  User.create!(name: "Outer")

  begin
    ActiveRecord::Base.transaction do
      User.create!(name: "Inner")
      raise "Something went wrong"
    end
  rescue => e
    puts "Rescued: #{e.message}"
  end

  User.create!(name: "After inner")
end        

Result:

  • Outer and After inner are saved
  • The Inner record is rolled back

📌 The outer transaction remains alive after the error inside the nested transaction — unless you explicitly raise again.


🔥 Gotchas (Things That Can Go Wrong)

  • If you assume an inner rollback cancels everything, your data might be left in a half-updated state.
  • If you rescue an error and continue silently, you may save data that depends on something that failed.
  • Side effects like emails, API calls, background jobs may run based on incorrect assumptions.


✅ When Nested Transactions Are Actually Useful

🎯 Real-World Case: Optional User Setup

ActiveRecord::Base.transaction do
  user = User.create!(name: "Lucas")

  begin
    ActiveRecord::Base.transaction do
      Settings.create!(user: user, notifications: true)
      Preferences.create!(user: user, theme: "dark")
      raise "Failed to fetch external config"
    end
  rescue => e
    Rails.logger.warn("Optional setup failed: #{e.message}")
    # Continue: user is valid even if settings fail
  end

  WelcomeMailer.send_welcome_email(user)
end        

Why this works:

  • You isolate optional logic in a safe block.
  • If something fails, only the optional settings are rolled back.
  • The user is still valid and continues through the flow.

✅ This is a good use of nested transactions.


🚫 But Be Careful…

  • If the setup was not truly optional, you just silently skipped something critical.
  • If you send emails or trigger jobs after a partial rollback, you're leaking inconsistent state.
  • If someone later assumes `user.settings` will exist, bugs will follow.


🧠 SAVEPOINTs (Advanced Use)

Rails exposes savepoint control via `connection`:

ActiveRecord::Base.transaction do
  connection = ActiveRecord::Base.connection

  connection.create_savepoint
  begin
    # risky operation
  rescue
    connection.rollback_to_savepoint
  ensure
    connection.release_savepoint
  end
end        

⚙️ This gives fine-grained control, but for most use cases, Rails' built-in .transaction with error handling is enough.


✅ Recommendations

  1. Use nested transactions only when isolation is really needed Don't wrap `.transaction` just for code structure — each one creates overhead.
  2. Raise errors intentionally If something critical fails in the inner block, let the error bubble up to trigger a full rollback.
  3. Avoid side effects inside transactions Especially in inner blocks — they might run even after a partial rollback.
  4. Log errors, but know the consequences Logging + continuing means you’re taking responsibility for whatever comes next.
  5. Keep transactions short and focused Complex nested logic often means you're mixing responsibilities.


⚠️ Warnings

  • 🚫 Inner `.transaction` failures do not cancel the outer transaction by default.
  • 🕳️ Side effects run even after inner rollbacks if you don't stop the flow.
  • 🧪 Test behavior carefully: rescuing errors in tests may hide real bugs.
  • 🔁 Rails uses SAVEPOINTs, but other DBs (like SQLite) may behave differently.


🧠 Summary

  • Rails simulates nested transactions with savepoints, not true sub-transactions.
  • If an inner block fails, it rolls back only that block, unless you raise the error again.
  • Used carefully, this can isolate optional steps — but if misused, it may cause silent bugs.
  • When in doubt, raise early and keep your transaction logic clear and explicit.


📌 Coming next in the series: How to handle side effects, external calls, and background jobs in transactional flows — without losing data integrity or sending wrong emails.

Eyji K.

Software Engineer | Python, Django, AWS, RAG

3w

Thanks for sharing, Fabio

Like
Reply
Fernando Miyahira

Software Engineer | Mobile Developer | Flutter | Dart

1mo

Thanks for sharing!

Higor Mesquita

SDET | QA Engineer | Test Automation Engineer | Playwright | Cypress | Robot Framework | Postman | Cucumber | Jenkins | Typescript | Javascript | Python | Manual Testing | Jira

1mo

💡 Great insight.

Ricardo Barioni

AI Engineer | Python | Computer Vision | Generative AI | LLM | Data Science | M.Sc.

1mo

Excellent breakdown. Savepoints are powerful but easy to misuse if you're not aware of how Rails handles nested transactions.

Johnny Hideki

.NET Software Engineer | Full Stack Developer | C# | React | Azure

1mo

Excellent article!

To view or add a comment, sign in

Others also viewed

Explore topics