Prerequisites
This tutorial assumes you have the Accounting module included in your project:Core Concepts
Accounts
Accounts are classified by type:- ASSET - Resources owned (cash, receivables)
- LIABILITY - Obligations owed (payables, loans)
- REVENUE - Income earned
- EXPENSE - Costs incurred
- OFF_BALANCE - Memo accounts not part of the balance sheet
Transactions
Transactions follow double-entry principles:- Every transaction has credits (increases) and debits (decreases)
- Credits and debits must balance (sum to zero) for on-balance accounts
- Transactions have two timestamps:
occurredAt- when the event happened in realityappliesAt- when it affects the books (bi-temporal accounting)
Step-by-Step Tutorial
import com.softwarearchetypes.accounting.*;
import java.time.*;
Instant now = LocalDateTime.of(2024, 3, 15, 10, 0)
.atZone(ZoneId.systemDefault())
.toInstant();
Clock clock = Clock.fixed(now, ZoneId.systemDefault());
AccountingConfiguration config = AccountingConfiguration.inMemory(clock);
AccountingFacade facade = config.facade();
The clock represents “now” for your accounting system. In production, use
Clock.systemDefaultZone().import static com.softwarearchetypes.accounting.CreateAccount.*;
// Create asset accounts
AccountId cashAccount = AccountId.generate();
facade.createAccount(generateAssetAccount(cashAccount, "Cash"));
AccountId receivablesAccount = AccountId.generate();
facade.createAccount(generateAssetAccount(receivablesAccount, "Accounts Receivable"));
// Create liability account
AccountId payablesAccount = AccountId.generate();
facade.createAccount(
new CreateAccount(payablesAccount, "Accounts Payable", "LIABILITY")
);
// Create revenue account
AccountId salesAccount = AccountId.generate();
facade.createAccount(
new CreateAccount(salesAccount, "Sales Revenue", "REVENUE")
);
import com.softwarearchetypes.quantity.money.Money;
import java.util.*;
AccountId cash = AccountId.generate();
AccountId equity = AccountId.generate();
Set<CreateAccount> accounts = Set.of(
generateAssetAccount(cash, "Cash"),
new CreateAccount(equity, "Owner Equity", "LIABILITY")
);
AccountAmounts initialBalances = AccountAmounts.of(Map.of(
cash, Money.pln(10000), // Debit: increases asset
equity, Money.pln(-10000) // Credit: increases liability
));
Result<String, Set<AccountId>> result =
facade.createAccountsWithInitialBalances(accounts, initialBalances);
if (result.success()) {
System.out.println("Accounts created with balances");
}
TransactionBuilderFactory txFactory = config.transactionBuilderFactory();
Instant transactionTime = now.minusSeconds(3600); // 1 hour ago
Instant applyTime = now; // applies now
Transaction saleTx = txFactory.transaction()
.occurredAt(transactionTime)
.appliesAt(applyTime)
.withTypeOf("cash_sale")
.executing()
.debitFrom(cashAccount, Money.pln(1000)) // Cash increases
.creditTo(salesAccount, Money.pln(1000)) // Revenue increases
.build();
Result<String, TransactionId> result = facade.execute(saleTx);
if (result.success()) {
System.out.println("Transaction recorded: " + result.getSuccess());
}
AccountId commissionExpense = AccountId.generate();
facade.createAccount(
new CreateAccount(commissionExpense, "Commission Expense", "EXPENSE")
);
// Customer payment: 1000 PLN cash, 50 PLN commission, 950 PLN to revenue
Transaction complexSale = txFactory.transaction()
.occurredAt(now)
.appliesAt(now)
.withTypeOf("cash_sale_with_commission")
.executing()
.debitFrom(cashAccount, Money.pln(1000))
.debitFrom(commissionExpense, Money.pln(50))
.creditTo(salesAccount, Money.pln(1050))
.build();
facade.execute(complexSale);
// Current balance
Optional<Money> currentBalance = facade.balance(cashAccount);
currentBalance.ifPresent(balance ->
System.out.println("Current cash balance: " + balance)
);
// Balance at a specific point in time (bi-temporal)
Instant pastTime = now.minusSeconds(7200);
Optional<Money> historicalBalance = facade.balanceAsOf(cashAccount, pastTime);
historicalBalance.ifPresent(balance ->
System.out.println("Cash balance 2 hours ago: " + balance)
);
// Query multiple account balances at once
Balances balances = facade.balances(Set.of(cashAccount, receivablesAccount));
// Original transaction
Transaction originalTx = txFactory.transaction()
.occurredAt(now.minusSeconds(1800))
.appliesAt(now.minusSeconds(1800))
.withTypeOf("invoice")
.executing()
.debitFrom(receivablesAccount, Money.pln(500))
.creditTo(salesAccount, Money.pln(500))
.build();
facade.execute(originalTx);
// Create reversal (flips debits and credits)
Transaction reversalTx = txFactory.transaction()
.occurredAt(now)
.appliesAt(now)
.reverting(originalTx)
.build();
facade.execute(reversalTx);
System.out.println("Transaction reversed");
// Find a specific transaction
Optional<TransactionView> txView = facade.findTransactionBy(saleTx.id());
txView.ifPresent(tx -> {
System.out.println("Transaction type: " + tx.type());
System.out.println("Occurred at: " + tx.occurredAt());
System.out.println("Applied at: " + tx.appliesAt());
System.out.println("Entries: " + tx.entries().size());
});
// Find all transactions for an account
List<TransactionId> accountTransactions =
facade.findTransactionIdsFor(cashAccount);
System.out.println("Cash account has " +
accountTransactions.size() + " transactions");
Working with Off-Balance Accounts
Off-balance accounts are useful for tracking information that doesn’t appear on the balance sheet:Bi-Temporal Accounting Example
The power of bi-temporal accounting is separating when something happened from when you record it:Complete Example
Next Steps
- Learn about Entry Allocations for tracking partial payments
- Explore Transaction Validity for time-limited entries
- See E-commerce Scenarios for real-world patterns
