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:
Returns the unique entry identifier.
Unique identifier for this entry
transactionId
Returns the ID of the transaction containing this entry.
TransactionId transactionId()
ID of the parent transaction
accountId
Returns the ID of the account this entry affects.
amount
Returns the monetary amount of the entry.
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.
Real-world timestamp when the entry occurred
appliesAt
Returns when the entry applies for accounting purposes.
Accounting timestamp determining when this entry affects balances
Returns metadata associated with the entry.
Key-value metadata for additional context
validity
Returns the validity period for this entry.
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()
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
)
Target account for the credit
Credit amount (positive value)
Defaults:
id: Auto-generated
metadata: Empty
validity: Always valid
appliedTo: Empty
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
)
Time period when this credit is valid
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
)
Target account for the debit
Debit amount (positive value, will be negated when applied)
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
)
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()
}
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
-
Use TransactionBuilder: Create entries through the builder, not directly
-
Match Credits to Debits: Use entry allocations to track which entries offset each other
-
Set Validity for Temporary Entries: Use validity periods for holds, reservations, or temporary allocations
-
Include Metadata: Attach business context for auditing and reporting
-
Understand Amount Signs:
- Credits: Positive amounts
- Debits: Negative amounts (automatically negated)
-
Query via Views: Use
AccountView and TransactionView to retrieve entry information