Architecture Overview
Ledgerly is designed as a layered system that separates responsibilities into small, focused components.
This architecture ensures:
- Predictable behavior
- Extensibility
- Testability
- Clear public API boundaries
This page provides a high-level overview of how Ledgerly works internally.
High-Level Flow
When you log an entry:
ledgerly()
->actor($user)
->action('billing.invoice.updated')
->target($invoice)
->log();
Ledgerly performs the following steps:
- Collects builder state (actor, action, target, metadata, diff)
- Resolves metadata using the metadata pipeline
- Merges transaction and context metadata
- Validates the payload
- Creates an immutable LedgerEntry
- Stores it in the database
- Resets builder state
Each step is handled by a separate component.
Core Components
Ledgerly Core is structured into several main parts.
Builder
The builder provides the public API:
ledgerly()->action(...)->log();
Responsibilities:
- Collect runtime data
- Coordinate logging
- Delegate work to other components
The builder does not perform validation or persistence directly.
EntryPayload
EntryPayload is a data object that represents a complete ledger entry before persistence.
Responsibilities:
- Encapsulate entry data
- Provide a stable structure
- Decouple builder from storage
This improves testability and separation of concerns.
EntryValidator
The validator ensures entries are valid before being stored.
Validation includes:
- Action format
- Diff structure
- Metadata structure
- Required fields
Invalid entries throw exceptions to prevent bad data.
LedgerEntryFactory
The factory converts payloads into Eloquent models.
Responsibilities:
- Create LedgerEntry instances
- Apply casts and defaults
- Persist entries
This keeps persistence logic separate from the builder.
Metadata Pipeline
The metadata pipeline collects metadata from multiple sources.
Resolvers may include:
- Source resolver
- Request resolver
- Job resolver
- Context resolver
- Environment resolver
Resolvers are configurable and executed in order.
MetadataAssembler
The assembler merges metadata from multiple layers:
- Pipeline metadata
- Transaction metadata
- Context metadata
- Explicit metadata
Later layers override earlier ones.
This ensures deterministic behavior.
ContextStore
ContextStore maintains runtime context values.
Responsibilities:
- Store context layers
- Support scoped context
- Restore context automatically
- Provide values to metadata resolvers
Context uses a stack-based design to support nesting safely.
TransactionManager
TransactionManager handles:
- Correlation identifiers
- Transaction lifecycle
- Nested transactions
- Duration tracking
Transactions are lightweight and safe to use frequently.
Metadata Resolvers
Resolvers are small classes responsible for supplying metadata.
Examples:
- SourceMetadataResolver
- RequestMetadataResolver
- JobMetadataResolver
- EnvironmentMetadataResolver
Resolvers are configured via:
config/ledgerly.php
Applications can register custom resolvers.
Public API vs. Internal Components
Ledgerly defines a clear boundary.
Public API (Stable)
These are safe to depend on:
- ledgerly() helper
- Builder methods
- LedgerEntry model
- Query scopes
- Export methods
- Events
- Configuration
Internal Components
These are not part of the public API and may change:
- Factories
- Validators
- Assemblers
- Internal services
Internal classes are marked with @internal.
Metadata Flow Diagram
Conceptually, metadata flows like this:
Resolvers
↓
Transaction metadata
↓
Context metadata
↓
Explicit metadata
↓
Stored in entry
Each step overrides earlier values if keys conflict.
Logging Lifecycle
A simplified lifecycle:
Builder collects data
→ Metadata resolved
→ Payload built
→ Payload validated
→ Entry persisted
→ Builder reset
Resetting the builder ensures the state does not leak between logs.
Extensibility Points
Ledgerly is designed to be extended safely.
Common extension points:
- Metadata resolvers
- Actor resolvers
- Severity mapping
- UI integrations
- Export pipelines
These extension points allow customization without modifying the core.
Design Principles
Ledgerly follows several design principles:
Immutability
Entries cannot be modified or deleted.
Deterministic Behavior
Metadata merging and transactions behave predictably.
Separation of Concerns
Each component has a single responsibility.
Laravel-Native Design
Ledgerly uses familiar Laravel patterns:
- Service container
- Config-driven behavior
- Eloquent models
- Artisan commands
Future Architecture
Ledgerly is designed to support:
- UI visualization
- Cloud aggregation
- Multi-application logging
- Analytics pipelines
The core architecture is intentionally small and focused to support these layers.
Next Step
Continue to: