CodeBlanks Logo Icon CodeBlanks

Challenge 4 - Database

Write safe data with transaction and prepared statement.

Challenge: a safe transfer

You will write the function that handles a money transfer in a banking service. Use this interface instead of a real database:

type Tx interface {
    Exec(sorgu string, args ...any) error
    Commit() error
    Rollback() error
}

Rules: the amount must be positive; the withdrawal and deposit must be in the same transaction; if any step fails, Rollback must be called; queries must use placeholders.

Testing with a fake Tx

Thanks to the interface you can write a fake Tx that records which calls were made:

type sahteTx struct {
    log    []string
    hataAt int // which Exec should fail (0 = none)
}

func (t *sahteTx) Exec(q string, a ...any) error {
    t.log = append(t.log, "exec")
    if len(t.log) == t.hataAt { return errors.New("db hatası") }
    return nil
}

In the test you verify that log is [exec exec commit] or [exec rollback].

Practice The transfer function

Write transfer(tx Tx, kimden, kime, tutar int) error following the rules. Run two scenarios with the fake Tx: no error, and an error on the second Exec. Print each scenario's log on its own line.

Starter code

main.go
package main

import (
    "errors"
    "fmt"
)

func main() {
    // buraya yaz
}

Sample solution

main.go
package main

import (
    "errors"
    "fmt"
)

type Tx interface {
    Exec(sorgu string, args ...any) error
    Commit() error
    Rollback() error
}

type sahteTx struct {
    log    []string
    hataAt int
}

func (t *sahteTx) Exec(q string, a ...any) error {
    t.log = append(t.log, "exec")
    if len(t.log) == t.hataAt {
        return errors.New("db hatası")
    }
    return nil
}

func (t *sahteTx) Commit() error   { t.log = append(t.log, "commit"); return nil }
func (t *sahteTx) Rollback() error { t.log = append(t.log, "rollback"); return nil }

func transfer(tx Tx, kimden, kime, tutar int) error {
    if tutar <= 0 {
        return errors.New("geçersiz tutar")
    }
    if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
        tx.Rollback()
        return err
    }
    if err := tx.Exec("UPDATE hesap SET bakiye = bakiye + ? WHERE id = ?", tutar, kime); err != nil {
        tx.Rollback()
        return err
    }
    return tx.Commit()
}

func main() {
    a := &sahteTx{}
    transfer(a, 1, 2, 100)
    b := &sahteTx{hataAt: 2}
    transfer(b, 1, 2, 100)
    fmt.Println(a.log)
    fmt.Println(b.log)
}

Expected output

[exec exec commit]
[exec exec rollback]

Questions and answers

Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.

1 If the amount is negative, what is the right behavior?

  • Return an error without touching the transaction Correct answer
  • Withdraw first, then check
  • Use the absolute value of the amount
  • Commit and log a warning

Explanation: Validation happens before any work; with invalid input no database operation should start.

2 Roll back on error

Question
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
    tx.____()
    return err
}
Answer
if err := tx.Exec("UPDATE hesap SET bakiye = bakiye - ? WHERE id = ?", tutar, kimden); err != nil {
    tx.Rollback()
    return err
}

Explanation: On error, Rollback undoes all changes inside the transaction.

3 Case and outcome

Term Meaning
Amount ≤ 0 Error, don't touch Tx
Exec error Rollback
Query parameter The ? placeholder
Both succeed Commit

All lessons in Advanced