# Transactions

> Run writes in lagoon.Transaction, defer side effects with lagoon.AfterCommit until the commit, and install GORM callbacks from Boot with lagoon.OnDatabase.

Laravel's `DB::transaction` runs a closure in a transaction, and `DB::afterCommit` defers work until it commits. [lagoon](/docs/api/lagoon.md) has the same pair, `lagoon.Transaction` and `lagoon.AfterCommit`, and the rest of the framework relies on them: realtime broadcasts, search index updates and blob deletions wait for the commit, so no client hears about a row that was rolled back.

## Running a transaction

`lagoon.Transaction` runs a function in a transaction and commits when it returns `nil`. Inside the function, use the `ctx` and `tx` it receives for every write. Work registered with `lagoon.AfterCommit` runs, in registration order, only after the commit succeeds; when the function returns an error, the transaction rolls back and the work is dropped:

```go
return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
	if err := tx.Model(&Post{}).Where("id = ?", id).Update("title", "Published").Error; err != nil {
		return err
	}
	lagoon.AfterCommit(ctx, tx, func(ctx context.Context, db *gorm.DB) {
		*log = append(*log, fmt.Sprintf("post %d published", id)) // broadcast, index, send mail...
	})
	if fail {
		return errors.New("rolled back") // the AfterCommit work never runs
	}
	return nil
})
```

The callback receives a database handle with an empty statement, so a query it runs never continues from the written model's statement. A panicking callback is logged and does not turn a committed write into an error.

`lagoon.AfterCommit` behaves differently depending on where it is called:

| Called | The work runs |
|--------|---------------|
| Inside `lagoon.Transaction` | After the outermost transaction commits; never after a rollback. |
| In a GORM callback of a single-statement write (GORM's own implicit transaction) | After GORM commits that write; never when the write fails. |
| Inside a plain `gorm.DB.Transaction` or another transaction lagoon did not open | Never. lagoon cannot see whether that transaction commits, so it logs a warning and skips the work. |
| Outside any transaction | Immediately. |

The third row is deliberate: running the work early could announce a write that later rolls back. When code in a transaction needs after-commit work, open the transaction with `lagoon.Transaction`.

## Nested transactions

A `lagoon.Transaction` inside another becomes a savepoint. Its after-commit work joins the outer transaction's only when its own function succeeds, so work dropped with a failed savepoint never runs:

```go
return lagoon.Transaction(ctx, db, func(ctx context.Context, tx *gorm.DB) error {
	lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "outer") })

	// A nested Transaction is a savepoint. Pass it the outer tx: given
	// the root db handle it returns an error instead.
	_ = lagoon.Transaction(ctx, tx, func(ctx context.Context, tx *gorm.DB) error {
		lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "dropped") })
		return errors.New("savepoint rolled back")
	})
	return lagoon.Transaction(ctx, tx, func(ctx context.Context, tx *gorm.DB) error {
		lagoon.AfterCommit(ctx, tx, func(context.Context, *gorm.DB) { *log = append(*log, "inner") })
		return nil
	})
})
```

Pass the nested call the outer transaction's `tx`. Given the root database handle instead, the nested `lagoon.Transaction` returns an error without running its function. Otherwise it would open a second, independent transaction whose after-commit work would wait for the outer one.

## Callbacks registered at boot

A hook that calls a service, such as a broadcast or a job dispatch, is a GORM callback rather than a model method (see [Models](/docs/database/models.md)). A plugin registers it from `Boot`, but `Boot` runs before the `serve` command opens the database. `lagoon.OnDatabase` bridges the gap: it runs your function as soon as the database is published, immediately when it already is.

Register the callback before GORM's `gorm:commit_or_rollback_transaction` step and defer its side effect with `lagoon.AfterCommit`:

```go
return lagoon.OnDatabase(app, func(_ *sql.DB, gdb *gorm.DB) error {
	return gdb.Callback().Create().After("gorm:create").Before("gorm:commit_or_rollback_transaction").Register("acme:post_created", func(db *gorm.DB) {
		post, ok := db.Statement.Dest.(*Post)
		if db.Error != nil || !ok {
			return
		}
		lagoon.AfterCommit(db.Statement.Context, db, func(ctx context.Context, db *gorm.DB) {
			*log = append(*log, "created "+post.Slug) // runs only once the insert is committed
		})
	})
})
```

> [!WARNING]
> Register such a callback with a `Before("gorm:commit_or_rollback_transaction")` constraint, as above. lagoon runs a single-statement write's after-commit work from its own callback right after that commit step; a callback that GORM sorts after it buffers work that is never run, without an error.

## Boot-time work that needs the database

`lagoon.OnDatabase` is also the place for anything else a plugin must do with the database handle at start-up, such as registering a join table with `lagoon.RegisterJoinTable`. The error of a queued function is returned by `lagoon.Publish`, so a failing callback stops the start-up.
