Architecture
This document provides a deep dive into the architecture of the Book Keeper application. It is designed to be a scalable, auditable, and flexible financial engine built on modern software design principles.
Core Principles
The system's architecture is founded on three core principles:
- Domain-Driven Design (DDD): We model the software closely on the real-world business domain of accounting. This is achieved by organizing the system into distinct Bounded Contexts, each with its own ubiquitous language and clear responsibilities.
- Command Query Responsibility Segregation (CQRS): The system separates the models and operations for writing data (Commands) from those for reading data (Queries). This allows each side to be optimized independently for its specific task, leading to higher performance and scalability.
- Event Sourcing: Instead of storing the current state of data, we store the full sequence of immutable events that have occurred over time. The current state is derived by replaying these events. This provides a complete, unchangeable audit log of every action taken in the system, which is invaluable for financial applications.
Bounded Contexts
The application is divided into the following Bounded Contexts to manage complexity and maintain clear boundaries:
-
Ledger Context: This is the heart of the system. It is responsible for all write-side operations, including the creation, validation, and posting of double-entry journal transactions. Its primary concern is ensuring the integrity and balance of the ledger.
-
Reporting Context: This is the primary read-side context. It subscribes to events published by the
Ledgercontext and builds denormalized read models (projections) optimized for querying. It is responsible for generating all financial reports, such as the General Ledger, Trial Balance, and Balance Sheet. -
Compliance Context: A supporting context designed to encapsulate country-specific business rules, tax laws, and validation logic. It operates asynchronously, reacting to events like
JournalEntryPostedto check for compliance without blocking the core transactional flow.
Layered Architecture
Each Bounded Context follows a clean, layered architecture inspired by Hexagonal (Ports and Adapters) and Onion Architecture. This ensures a strong separation of concerns and makes the system highly testable and maintainable.
- Domain Layer: The innermost layer. It contains the core business logic, entities (Aggregates), value objects, and domain events. This layer is completely independent of any technical implementation details.
- Application Layer: This layer orchestrates the domain logic to fulfill application-specific use cases. It uses Commands and Queries to interact with the domain layer.
- Infrastructure Layer: This layer provides concrete implementations for interfaces (ports) defined in the layers above. This includes repositories, event stores, external service clients, and message bus adapters.
- Presentation Layer: The outermost layer, which exposes the application's functionality to the outside world. In this project, it is a REST API built with FastAPI.
The fundamental rule is the Dependency Inversion Principle: outer layers depend on abstractions defined in inner layers, never the other way around.
System Integration Patterns
Event-Driven Choreography
The primary pattern for communication between Book Keeper and other microservices (e.g., Invoicing, Payroll) is event-driven choreography.
- How it works: External services publish business-significant domain events (e.g.,
InvoicePaid,PayrollRunCompleted) to a shared event bus.Book Keepersubscribes to these events and translates them into internal commands. - Rationale: This creates a loosely coupled system. The
Invoicingservice doesn't need to know anything about accounting; it simply states the fact that an invoice was paid. This respects service autonomy and increases system resilience, asBook Keepercan process events from a queue even if it was temporarily offline.
Anti-Corruption Layer (ACL)
To protect the integrity of our core domain, all incoming external events are processed through an Anti-Corruption Layer (ACL).
- Role: The ACL acts as a translation layer. It consists of dedicated
Translatorcomponents, each responsible for converting a specific external event into aBook Keeper-native command. - Benefit: This ensures that the core domain is never polluted with the data models or concepts of external systems, maintaining its purity and focus.
Infrastructure & Extensibility
A key goal of the architecture is to be flexible and adaptable to different operational environments. This is achieved through a swappable, adapter-based infrastructure.
The application depends on abstract interfaces (e.g., IEventStore, ILedgerStorageService, IEventBus) for its storage and messaging needs, not on concrete implementations. The specific technology for each interface can be selected via configuration at runtime.
For a detailed list of supported technologies, see the Technology Stack documentation.
Plugin System for Extensibility
To allow for deep customization without modifying the core codebase, the system supports a plugin model using Python's standard entry_points mechanism. This allows external teams to develop and "plug in" their own components, such as custom Translators for new microservices or even alternative infrastructure adapters.
Cross-Cutting Concerns
Multi-Tenancy
The system is designed from the ground up to be multi-tenant, allowing it to securely manage data for multiple distinct companies or entities.
- Implementation: A
TenantIdis a first-class citizen in the domain. It is included in all key aggregates, commands, and events. - Data Isolation: All database queries, on both the write-side (event store) and read-side (reporting projections), are strictly filtered by
TenantId. This is enforced at the repository level, guaranteeing that one tenant can never access another's data.
Security: Authentication & Authorization
Security is handled with a layered approach:
- Authentication (AuthN): Handled at the network edge by an API Gateway or service mesh sidecar. It is responsible for validating JWTs and rejecting unauthenticated requests.
- Authorization (AuthZ): Performed within the application for fine-grained control.
- Engine: We use Casbin as our internal authorization engine.
- Logic: Authorization checks are performed inside each command and query handler. This allows for context-aware rules, such as verifying that a user has permission to perform an action on a resource within their own tenant.
External Dependencies & Data Management
Chart of Accounts (CoA)
The management of the Chart of Accounts is a classic master data problem. To keep the Book Keeper's domain pure, this responsibility is delegated.
- External Service: The CoA is managed by a dedicated, external
Accounts Mastermicroservice. This service is the single source of truth for creating and managing accounts. - Asynchronous Interaction:
Book Keeperdoes not perform synchronous, blocking calls to theAccounts Masterto validate account codes. This would create tight coupling and reduce system availability. - Eventual Consistency: Instead, the
Reportingcontext subscribes to events from both theLedger(JournalEntryPosted) and theAccounts Master(AccountCreated). It enriches reports with account names and can identify transactions that use an account code for which no master data exists. This reconciliation happens asynchronously.