Skip to main content

Overview

The Entry sealed interface represents individual accounting entries that record financial movements. Each entry is either a debit (AccountDebited) or credit (AccountCredited) to a specific account. Entries are created as part of transactions and cannot exist independently. Package: com.softwarearchetypes.accounting Access: Public sealed interface permitting AccountDebited and AccountCredited

Interface Definition

sealed interface Entry permits AccountDebited, AccountCredited {
    EntryId id();
    TransactionId transactionId();
    Instant occurredAt();
    Instant appliesAt();
    AccountId accountId();
    Money amount();
    MetaData metadata();
    Validity validity();
    Optional<EntryId> appliedTo();
}

Common Properties

All entry types implement these methods:

id

Returns the unique entry identifier.
EntryId id()
EntryId
EntryId
Unique identifier for this entry

transactionId

Returns the ID of the transaction containing this entry.
TransactionId transactionId()
TransactionId
TransactionId
ID of the parent transaction

accountId

Returns the ID of the account this entry affects.
AccountId accountId()
AccountId
AccountId
ID of the target account

amount

Returns the monetary amount of the entry.
Money amount()
Money
Money
Amount with currency. Positive for credits, negative for debits.
Important:
  • AccountCredited returns the amount as-is (positive)
  • AccountDebited returns the amount negated (negative)

occurredAt

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

appliesAt

Returns when the entry applies for accounting purposes.
Instant appliesAt()
Instant
Instant
Accounting timestamp determining when this entry affects balances

metadata

Returns metadata associated with the entry.
MetaData metadata()
MetaData
MetaData
Key-value metadata for additional context

validity

Returns the validity period for this entry.
Validity validity()
Validity
Validity
Time period during which this entry is valid (defaults to “always” if not specified)
Use Case: Temporary holds or allocations that expire after a period.

appliedTo

Returns the ID of another entry this one is applied to (for entry allocations).
Optional<EntryId> appliedTo()
Optional
Optional<EntryId>
Reference to another entry, or empty if this entry is not an allocation
Use Case: Tracking which credit entry a debit entry is consuming (e.g., payment applied to specific invoice).

AccountCredited

Represents a credit entry (increases account balance for asset accounts).
record AccountCredited(
    EntryId id,
    TransactionId transactionId,
    AccountId accountId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata,
    Validity validity,
    Optional<EntryId> appliedTo
) implements Entry

Constructors

Basic Credit

AccountCredited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt
)
accountId
AccountId
required
Target account for the credit
transactionId
TransactionId
required
Parent transaction ID
amount
Money
required
Credit amount (positive value)
appliesAt
Instant
required
When credit applies
occurredAt
Instant
required
When credit occurred
Defaults:
  • id: Auto-generated
  • metadata: Empty
  • validity: Always valid
  • appliedTo: Empty

Credit with Metadata

AccountCredited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata
)

Credit with Validity

AccountCredited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata,
    Validity validity,
    EntryId appliedToEntryId
)
validity
Validity
required
Time period when this credit is valid
appliedToEntryId
EntryId
Reference to another entry (can be null)

Example Usage

// Simple credit via TransactionBuilder
facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("revenue")
    .executing()
    .creditTo(revenueAccount, Money.of(1000, "PLN"))
    .debitFrom(cashAccount, Money.of(1000, "PLN"))
    .build();

// Credit with validity period
Validity validity = Validity.between(
    Instant.now(),
    Instant.now().plus(30, ChronoUnit.DAYS)
);

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

AccountDebited

Represents a debit entry (decreases account balance for asset accounts).
record AccountDebited(
    EntryId id,
    TransactionId transactionId,
    AccountId accountId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata,
    Validity validity,
    Optional<EntryId> appliedTo
) implements Entry

Special Behavior

Important: The amount() method returns the negated amount:
@Override
public Money amount() {
    return amount.negate();
}
This ensures debits contribute negative values to account balances.

Constructors

Basic Debit

AccountDebited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt
)
accountId
AccountId
required
Target account for the debit
transactionId
TransactionId
required
Parent transaction ID
amount
Money
required
Debit amount (positive value, will be negated when applied)
appliesAt
Instant
required
When debit applies
occurredAt
Instant
required
When debit occurred

Debit with Metadata

AccountDebited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata
)

Debit with Entry Allocation

AccountDebited(
    AccountId accountId,
    TransactionId transactionId,
    Money amount,
    Instant appliesAt,
    Instant occurredAt,
    MetaData metadata,
    Validity validity,
    EntryId appliedToEntryId
)
appliedToEntryId
EntryId
ID of a credit entry this debit is applied against
Use Case: Recording that a payment (debit to cash) is applied to a specific invoice (credit to receivables).

Example Usage

// Simple debit via TransactionBuilder
facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("expense")
    .executing()
    .debitFrom(expenseAccount, Money.of(500, "PLN"))
    .creditTo(cashAccount, Money.of(500, "PLN"))
    .build();

// Debit applied to specific credit entry
facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("payment")
    .executing()
    .debitFrom(receivablesAccount, Money.of(1000, "PLN"), specificCreditEntryId)
    .creditTo(cashAccount, Money.of(1000, "PLN"))
    .build();

Entry Allocations

Entry allocations track which debits are applied to which credits (or vice versa). This is useful for:
  • Matching payments to invoices
  • Tracking which expenses consume which budget allocations
  • Implementing aging reports based on unallocated entries

Creating Allocated Entries

// Credit entry (e.g., invoice)
Transaction invoice = facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("invoice")
    .executing()
    .debitFrom(receivablesAccount, Money.of(1000, "PLN"))
    .creditTo(revenueAccount, Money.of(1000, "PLN"))
    .build();

facade.execute(invoice);

// Later: Payment applied to specific invoice entry
EntryId invoiceEntryId = /* retrieve from invoice transaction */;

Transaction payment = facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .withTypeOf("payment")
    .executing()
    .debitFrom(receivablesAccount, Money.of(1000, "PLN"), invoiceEntryId)
    .creditTo(cashAccount, Money.of(1000, "PLN"))
    .build();

facade.execute(payment);

Finding Allocated Entries

Use EntryRepository.findEntriesReferencing() to find all entries that reference a specific entry:
List<Entry> allocatedPayments = entryRepository.findEntriesReferencing(invoiceEntry);

Entry Validity

Entries can have validity periods using the Validity value object:
// Entry valid for 30 days
Validity thirtyDays = Validity.between(
    Instant.now(),
    Instant.now().plus(30, ChronoUnit.DAYS)
);

// Entry valid forever
Validity forever = Validity.always();

// Check if valid at specific time
boolean isValid = validity.isValidAt(Instant.now());
boolean hasExpired = validity.hasExpired(clock.instant());

Expiration Compensation

When entries expire, you can automatically compensate:
facade.transaction()
    .occurredAt(Instant.now())
    .appliesAt(Instant.now())
    .compensatingExpired(expiredEntryId)
    .withCompensationAccount(compensationAccountId)
    .build()
    .ifPresent(facade::execute);
This creates a transaction that reverses the remaining unallocated amount of the expired entry.

EntryView

Read model for querying entry data:
public record EntryView(
    EntryId entryId,
    EntryType type,
    Money amount,
    TransactionId transactionId,
    AccountId accountId,
    Instant occurredAt,
    Instant appliesAt
) {
    public enum EntryType {
        CREDIT,
        DEBIT
    }
}

Retrieving Entries

// Get all entries for an account
Optional<AccountView> accountView = facade.findAccount(accountId);
accountView.ifPresent(view -> {
    List<EntryView> entries = view.entries();
    entries.forEach(entry -> {
        System.out.println(entry.type() + ": " + entry.amount());
    });
});

// Get entries for a transaction
Optional<TransactionView> txView = facade.findTransactionBy(transactionId);
txView.ifPresent(view -> {
    view.entries().forEach(accountEntries -> {
        System.out.println("Account: " + accountEntries.account().name());
        accountEntries.entries().forEach(entry -> {
            System.out.println("  " + entry.type() + ": " + entry.amount());
        });
    });
});

EntryId

public record EntryId(UUID value) {
    public static EntryId generate()
}

MetaData

Key-value map for storing additional entry context:
MetaData metadata = MetaData.of(
    "orderId", "12345",
    "customerId", "CUST-001"
);

Validity

Represents a time period:
Validity.always()  // No expiration
Validity.between(start, end)  // Specific period

Best Practices

  1. Use TransactionBuilder: Create entries through the builder, not directly
  2. Match Credits to Debits: Use entry allocations to track which entries offset each other
  3. Set Validity for Temporary Entries: Use validity periods for holds, reservations, or temporary allocations
  4. Include Metadata: Attach business context for auditing and reporting
  5. Understand Amount Signs:
    • Credits: Positive amounts
    • Debits: Negative amounts (automatically negated)
  6. Query via Views: Use AccountView and TransactionView to retrieve entry information

Build docs developers (and LLMs) love