Skip to main content

Overview

The Transaction class represents a financial transaction in the accounting system. Each transaction consists of multiple entries (debits and credits) applied to different accounts. Transactions enforce double-entry bookkeeping rules by default, ensuring that debits equal credits. Package: com.softwarearchetypes.accounting Access: Public final class (created via TransactionBuilder)

Constructor

Transaction(
    TransactionId id,
    TransactionId refId,
    TransactionType type,
    Instant occurredAt,
    Instant appliesAt,
    Map<Entry, Account> entries,
    TransactionEntriesConstraint transactionEntriesConstraint
)
id
TransactionId
required
Unique identifier for the transaction
refId
TransactionId
Reference to another transaction (e.g., for reversals). Can be null.
type
TransactionType
required
Type/category of transaction (e.g., “payment”, “invoice”, “reversal”)
occurredAt
Instant
required
When the transaction occurred in real time
appliesAt
Instant
required
When the transaction applies for accounting purposes (may differ from occurredAt)
entries
Map<Entry, Account>
required
Map of entries to their target accounts
transactionEntriesConstraint
TransactionEntriesConstraint
required
Constraint to validate entries (e.g., balancing constraint)
Note: Use TransactionBuilder instead of calling this constructor directly.

Properties

id

Returns the unique transaction identifier.
public TransactionId id()
TransactionId
TransactionId
The transaction’s unique identifier

refId

Returns the reference transaction ID if this transaction references another.
Optional<TransactionId> refId()
Optional
Optional<TransactionId>
Reference transaction ID for reversals or related transactions, or empty

type

Returns the transaction type.
TransactionType type()
TransactionType
TransactionType
The type/category of this transaction
Common Types:
  • INITIALIZATION - Opening balances
  • REVERSAL - Reversal of previous transaction
  • EXPIRATION_COMPENSATION - Compensating expired entries
  • Custom types: “transfer”, “payment”, “invoice”, etc.

occurredAt

Returns when the transaction occurred in real time.
Instant occurredAt()
Instant
Instant
Real-world timestamp of when the transaction occurred

appliesAt

Returns when the transaction applies for accounting purposes.
Instant appliesAt()
Instant
Instant
Accounting timestamp determining when this transaction affects balances
Use Case: A transaction might occur on Jan 31 but apply on Feb 1 for accounting period purposes.

accountsInvolved

Returns all accounts affected by this transaction.
Set<Account> accountsInvolved()
Set
Set<Account>
All accounts that have entries in this transaction

entries

Returns all entries grouped by account.
Map<Account, List<Entry>> entries()
Map
Map<Account, List<Entry>>
Immutable map of accounts to their entries in this transaction

Methods

execute

Executes the transaction by applying all entries to their accounts.
void execute()
Side Effects:
  • Adds entries to each involved account
  • Updates account balances
  • Generates domain events for each entry
Note: Package-private method. Use AccountingFacade.execute() instead.

Transaction Constraints

Transactions are validated using TransactionEntriesConstraint implementations:

BALANCING_CONSTRAINT

Ensures that debits equal credits for double-entry accounts.
TransactionEntriesConstraint.BALANCING_CONSTRAINT
Validation: Sum of all entries on double-entry accounts must equal zero. Error Message: “Entry balance within transaction must always be 0” Note: OFF_BALANCE accounts are excluded from this constraint.

MIN_2_ENTRIES_CONSTRAINT

Ensures at least two entries per transaction.
TransactionEntriesConstraint.MIN_2_ENTRIES_CONSTRAINT
Validation: Transaction must have at least 2 entries. Error Message: “Transaction must have at least 2 entries”

MIN_2_ACCOUNTS_INVOLVED_CONSTRAINT

Ensures at least two different accounts are involved.
TransactionEntriesConstraint.MIN_2_ACCOUNTS_INVOLVED_CONSTRAINT
Validation: Transaction must involve at least 2 distinct accounts. Error Message: “Transaction must involve at least 2 accounts”

Creating Transactions

Using TransactionBuilder

The recommended way to create transactions is through TransactionBuilder:
TransactionBuilder builder = facade.transaction();

Transaction transaction = builder
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("payment")
    .withMetadata("orderId", "12345", "customer", "ACME Corp")
    .executing()
    .debitFrom(cashAccount, Money.of(1000, "PLN"))
    .creditTo(revenueAccount, Money.of(1000, "PLN"))
    .build();

Result<String, TransactionId> result = facade.execute(transaction);

Simple Transfer

For basic transfers between two accounts:
Result<String, TransactionId> result = facade.transfer(
    fromAccount,
    toAccount,
    Money.of(500, "PLN"),
    Instant.now(),
    Instant.now()
);

Complex Transaction with Multiple Entries

Transaction complexTx = facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("invoice")
    .executing()
    .debitFrom(receivablesAccount, Money.of(1230, "PLN"))
    .creditTo(revenueAccount, Money.of(1000, "PLN"))
    .creditTo(vatAccount, Money.of(230, "PLN"))
    .build();

facade.execute(complexTx);

Transaction with Entry Validity

Entries can have validity periods:
Validity validity = Validity.between(
    Instant.now(),
    Instant.now().plus(30, ChronoUnit.DAYS)
);

Transaction tempTx = facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("temporary_hold")
    .executing()
    .creditTo(holdAccount, Money.of(500, "PLN"), validity)
    .debitFrom(cashAccount, Money.of(500, "PLN"))
    .build();

Reversing Transactions

To reverse a previously executed transaction:
Transaction reversal = facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .reverting(originalTransactionId)
    .build();

Result<String, TransactionId> result = facade.execute(reversal);
Behavior:
  • Automatically creates opposite entries for all original entries
  • Sets refId to the original transaction ID
  • Sets type to REVERSAL
  • Debits become credits and vice versa

Using ReverseTransactionCommand

ReverseTransactionCommand command = new ReverseTransactionCommand(
    originalTransactionId.value().toString(),
    Instant.now(),
    Instant.now()
);

Result<String, TransactionId> result = facade.handle(command);

Transaction Views

To query transaction details:
Optional<TransactionView> txView = facade.findTransactionBy(transactionId);

txView.ifPresent(view -> {
    System.out.println("Transaction: " + view.id());
    System.out.println("Type: " + view.type());
    System.out.println("Occurred: " + view.occurredAt());
    System.out.println("Applies: " + view.appliesAt());
    
    if (view.refId() != null) {
        System.out.println("References: " + view.refId());
    }
    
    view.entries().forEach(accountEntries -> {
        System.out.println("Account: " + accountEntries.account().name());
        accountEntries.entries().forEach(entry -> {
            System.out.println("  " + entry.type() + ": " + entry.amount());
        });
    });
});

TransactionId

public record TransactionId(UUID value) {
    public static TransactionId generate()
    public static TransactionId of(UUID uuid)
}

TransactionType

Value object representing transaction types:
TransactionType.of("payment")
TransactionType.INITIALIZATION
TransactionType.REVERSAL
TransactionType.EXPIRATION_COMPENSATION

TransactionView

Read model for querying transaction data:
public record TransactionView(
    TransactionId id,
    TransactionId refId,
    TransactionType type,
    Instant occurredAt,
    Instant appliesAt,
    List<TransactionAccountEntriesView> entries
)

Best Practices

  1. Use TransactionBuilder: Always create transactions through the builder pattern for proper validation
  2. Set Meaningful Types: Use descriptive transaction types for better reporting and auditing
  3. Include Metadata: Attach relevant business context using withMetadata()
  4. Distinguish occurredAt vs appliesAt:
    • occurredAt: When the event happened in reality
    • appliesAt: When it should affect accounting reports
  5. Handle Results: Always check Result return values for success/failure
  6. Use Transfers for Simple Cases: The transfer() method is simpler than building transactions manually
  7. Validate Before Execute: Let constraints validate your entries before execution
  • AccountingFacade - Main interface for executing transactions
  • Entry - Individual debit/credit entries
  • Account - Accounts affected by transactions
  • TransactionBuilder - Builder for constructing transactions

Build docs developers (and LLMs) love