Middle · Architecture and systems

One use case, two repositories, one invariant

A debit and a credit use separate repositories. Both have their own Commit call. Is the transfer atomic?

Try explaining this aloud before opening the answer. State your assumptions and trace a boundary case.

EXAMPLE · FRAGMENT UNLESS A COMPLETE PROGRAM IS SHOWN

go
package main

import "fmt"

type Account struct{ Balance int }

func transfer(from, to *Account, amount int) error {
	if from == to || amount <= 0 || from.Balance < amount {
		return fmt.Errorf("invalid transfer")
	}
	// Single-owner, in-memory teaching model, not a database transaction.
	from.Balance -= amount
	to.Balance += amount
	return nil
}
func main() {
	a, b := Account{10}, Account{4}
	fmt.Println(transfer(&a, &b, 7), a.Balance, b.Balance)
	fmt.Println(transfer(&a, &b, 7), a.Balance, b.Balance)
}
Reveal a strong answer
  1. A transport handler can parse a request correctly while the application still commits only half of a business operation. Place the invariant in one use case and make its transaction boundary explicit. The adapter translates HTTP and errors; it does not independently commit each repository call. A Go interface alone cannot make two stores atomic.

  2. Trace validation, ownership and commit separately. In a SQL implementation, begin once in the use case, pass the same *sql.Tx to participating operations, defer Rollback and check Commit. Do not call the original *sql.DB from a repository that was supposed to join the transaction. Keep HTTP status mapping outside the use case. The first call validates before changing either account and prints balances 3 and 11. The second call fails before changing either value. This is safe only under the explicitly stated single owner; concurrent callers and crashes require a real storage transaction or an equivalent concurrency protocol.

  3. A mutex can protect an in-memory invariant but cannot make process memory durable or roll back an external payment. Under concurrent SQL writes, test isolation, row predicates and lock ordering. An interface named UnitOfWork is not evidence that all its methods share the same transaction. Define whether a failed commit is known to have aborted or has an unknown outcome.

Common trap

Naming two methods Debit and Credit or wrapping them in a Go interface does not create an atomic commit.

Try it

Draw a failure immediately after the debit commits. Refactor the dependency boundary so all participating calls share the same transaction handle.

Worked solution

No. The application use case must own a single storage transaction and pass its handle into both participating repository operations. Validation happens before effects where possible, Commit is checked, and rollback is deferred for an unfinished transaction. If the stores cannot join one transaction, explicitly choose a distributed workflow and reconciliation policy rather than hide the gap behind an interface.

Change the constraint

How would a concurrent overdraft or an unknown Commit outcome change your test plan?

How did you do?

Self-assessment, not a compiler pass or interview score. Explain the reasoning unaided before marking understood.

Not marked. Progress stays on this browser, separate from course completion.

Primary references

Checked 2026-10-28. Use documentation for your pinned runtime, driver and tool version.