Skip to main content

SF-0277 · Coding · Medium

Can you write sample code for a Transaction Control statement?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

The canonical shape of transaction control in Apex is savepoint → try → DML → catch → rollback. Three lines of API surface, one straightforward pattern.

The minimal example

public class OrderService {
    public void createOrderWithLines(Order__c header, List<Line_Item__c> lines) {
        Savepoint sp = Database.setSavepoint();
        try {
            insert header;
            for (Line_Item__c li : lines) {
                li.Order__c = header.Id;
            }
            insert lines;
        } catch (Exception e) {
            Database.rollback(sp);
            throw new OrderException('Order creation failed: ' + e.getMessage(), e);
        }
    }

    public class OrderException extends Exception {}
}

What happens here:

  1. Database.setSavepoint() snapshots the database state.
  2. We insert the order header and the line items.
  3. If anything inside the try throws, we Database.rollback(sp) — both inserts are undone.
  4. We re-throw a typed exception so callers know exactly what failed.

A more realistic multi-step example

public class CustomerOnboarding {
    public void onboard(String name, String email, Decimal initialDeposit) {

        Savepoint sp = Database.setSavepoint();

        try {
            // Step 1: create the Account
            Account a = new Account(Name = name);
            insert a;

            // Step 2: create the primary Contact
            Contact c = new Contact(
                LastName = name,
                Email    = email,
                AccountId = a.Id
            );
            insert c;

            // Step 3: create an opening balance record
            Customer_Balance__c bal = new Customer_Balance__c(
                Account__c = a.Id,
                Balance__c = initialDeposit
            );
            insert bal;

            // Step 4: post a welcome platform event
            EventBus.publish(new Welcome_Event__e(AccountId__c = a.Id));

        } catch (DmlException dmle) {
            Database.rollback(sp);
            System.debug('DML failure during onboarding: ' + dmle.getMessage());
            throw new OnboardingException(
                'Onboarding failed for ' + name + ': ' + dmle.getDmlMessage(0), dmle
            );

        } catch (Exception e) {
            Database.rollback(sp);
            throw new OnboardingException('Unexpected error: ' + e.getMessage(), e);
        }
    }

    public class OnboardingException extends Exception {}
}

Notice the two catch blocks — a specific DmlException handler for save failures, and a generic Exception handler as the fallback. Both roll back; both re-throw with context.

Multiple savepoints — partial rollback

When the unit of failure is one step (not the whole operation), use multiple savepoints:

public void importBatch(List<Account> accounts, List<Contact> contacts) {

    Savepoint spStart = Database.setSavepoint();
    insert accounts;

    Savepoint spAfterAccounts = Database.setSavepoint();
    try {
        for (Contact c : contacts) {
            // assume c.Account.Name was used to find the right new Account
        }
        insert contacts;
    } catch (Exception e) {
        // Keep the Accounts, drop the Contacts
        Database.rollback(spAfterAccounts);
        System.debug('Contacts failed but accounts retained: ' + e.getMessage());
    }
}

Rolling back to spAfterAccounts undoes the insert contacts only — the accounts stay. Rolling back to spStart would undo everything.

Test class for the savepoint logic

@isTest
private class OrderServiceTest {

    @isTest
    static void rollsBackOnFailure() {
        Order__c order = new Order__c(Name = 'Test');
        List<Line_Item__c> bad = new List<Line_Item__c>{
            new Line_Item__c(Quantity__c = -1) // fails a validation rule
        };

        Test.startTest();
        try {
            new OrderService().createOrderWithLines(order, bad);
            System.assert(false, 'Should have thrown');
        } catch (OrderService.OrderException e) {
            // expected
        }
        Test.stopTest();

        System.assertEquals(0, [SELECT COUNT() FROM Order__c]);
        System.assertEquals(0, [SELECT COUNT() FROM Line_Item__c]);
    }
}

The assertEquals(0, ...) lines prove the savepoint rolled back the partial insert.

Common mistakes the test usually catches

  • Reusing record Ids after rollbackheader.Id is reset to null when its insert is rolled back. Any field that already stored the value is left pointing to nothing.
  • Forgetting to re-throw — silently swallowing the exception means the caller thinks the operation succeeded.
  • Doing DML in the catch block without a fresh savepoint — if it fails, you’re back to default all-or-nothing rollback.

What interviewers are really looking for

The three-line pattern is the minimum. The senior signal is: (1) typed exception classes (OrderException) instead of re-throwing the raw cause, (2) separating DmlException from generic Exception for better diagnostics, (3) acknowledging that callouts and Publish-Immediately events don’t roll back, (4) discarding rolled-back Ids instead of reusing them, (5) a test that asserts zero rows after a forced failure to prove the savepoint worked.

Verified against: Apex Developer Guide — Transaction Control. Last reviewed 2026-05-17.