Smart Domain Architecture Accounting System

repository·main·Indexed 41 days ago

https://github.com/re-engineering-domain-driven-design/accounting

An accounting system demonstrating the Smart Domain Architecture pattern for Domain-Driven Design (DDD). It focuses on explicit association modeling, distinguishing between conceptual aggregation and data lifecycles (aggregated vs. reference), and mapping domain models to JAX-RS REST APIs where URI structures mirror association relationships.

Tokens
2.4K
Snippets
4
Records
7
Agent score
2%

What's inside Smart Domain Architecture

  1. Understand the Smart Domain Architecture Pattern

    main

    The project implements the Smart Domain architecture pattern as a way to realize Domain-Driven Design (DDD). The core idea is to model relationships in the domain model as Association Objects (关联对象) rather than simple properties.

    Key principles include:

    • Modeling Associations as Objects: Instead of direct references, relationships are represented by interfaces that act as association objects.
    • Exposing API over Model: The domain model is mapped to a RESTful API where the URI structure mirrors the association relationships (e.g., /customers/{cid}/accounts/{aid}).
    • Separating Business Aggregates from Lifecycles: A critical distinction is made between business aggregation (how things are grouped conceptually) and technical lifecycles (how things are loaded from storage). You can represent a business aggregate via a URI even if the technical implementation uses a 'reference' lifecycle to avoid loading massive amounts of data into memory.
  2. Implement Association Objects using Interfaces

    main

    In the Smart Domain pattern, associations are modeled using interfaces. To maintain encapsulation, the pattern uses a Wide Interface (containing modification logic) internally and exposes a Narrow Interface (read-only) to the rest of the application.

    For example, a Customer entity might expose HasMany collections through specific internal interfaces that allow controlled updates (like add or update) while preventing arbitrary state changes.

    public class Customer implements Entity<String, CustomerDescription> {
        private SourceEvidences sourceEvidences;
        private Accounts accounts;
    
        public HasMany<String, SourceEvidence<?>> sourceEvidences() {
            return sourceEvidences;
        }
    
        public interface SourceEvidences extends HasMany<String, SourceEvidence<?>> {
            SourceEvidence<?> add(SourceEvidenceDescription description);
        }
    
        public interface Accounts extends HasMany<String, Account> {
            void update(Account account, Account.AccountChange change);
        }
    }
  3. Distinguish between Aggregation and Lifecycle in Associations

    main

    A critical distinction in this architecture is between conceptual aggregation and lifecycle semantics.

    • Aggregated Lifecycle: The associated objects are loaded into memory whenever the parent entity is loaded. This is suitable for small, tightly coupled collections (e.g., SourceEvidence and its Transactions). Implementation typically uses reengineering.ddd.mybatis.memory.EntityList.
    • Reference Lifecycle: The associated objects are fetched from the database only when requested. This is suitable for large collections where loading everything would be inefficient (e.g., an Account with millions of Transactions). Implementation typically uses reengineering.ddd.mybatis.database.EntityList.

    Note: A relationship can be conceptually aggregated (represented by a hierarchical URI) but use a reference lifecycle for performance. For example, /customers/{cid}/accounts/{aid}/transactions/{tid} represents a hierarchical relationship, but the transactions are fetched via reference to avoid memory issues.

  4. Manage Entity Lifecycles (Aggregate vs. Reference)

    main

    When implementing association objects, you must choose between two lifecycle patterns based on data volume and performance needs:

    1. Aggregate Lifecycle (Memory/Direct Association): The associated objects are loaded into memory simultaneously with the parent. Use this for small, tightly coupled collections (e.g., SourceEvidence and its Transactions). This is implemented using reengineering.ddd.mybatis.memory.EntityList.
    2. Reference Lifecycle (Database/Lazy Loading): The associated objects are loaded only when explicitly requested. Use this for large collections to avoid performance bottlenecks (e.g., an Account with thousands of Transactions). This is implemented using reengineering.ddd.mybatis.database.EntityList.

    Note: A business aggregate can still be represented via a URI (e.g., /customers/{cid}/accounts/{aid}/transactions/{tid}) even if the technical implementation uses a Reference lifecycle.

    // Aggregate Lifecycle implementation
    import reengineering.ddd.mybatis.memory.EntityList;
    public class SourceEvidenceTransactions extends EntityList<String, Transaction> implements SourceEvidence.Transactions {}
    
    // Reference Lifecycle implementation
    import reengineering.ddd.mybatis.database.EntityList;
    public class AccountTransactions extends EntityList<String, Transaction> implements Account.Transactions {}
  5. Map Domain Models to RESTful APIs using JAX-RS

    main

    The domain model is mapped to a RESTful API by converting the root association object into a root resource and subsequent entities into sub-resources. The URI structure follows the association paths defined in the domain.

    1. Root Resource: Created from the root association object (e.g., CustomersApi for /customers).
    2. Sub-resources: Entities are converted into API classes (e.g., CustomerApi) that provide access to their own associations (e.g., /customers/{id}/accounts).
    3. Association APIs: Association objects themselves become sub-resources (e.g., SourceEvidencesApi for /customers/{id}/source-evidences).
    // Root Resource
    @Path("/customers")
    public class CustomersApi {
        private Customers customers;
    
        @Inject
        public CustomersApi(Customers customers) { this.customers = customers; }
    
        @Path("{id}")
        public CustomerApi findById(@PathParam("id") String id) {
            return customers.findById(id).map(CustomerApi::new).orElse(null);
        }
    }
    
    // Entity Sub-resource
    public class CustomerApi {
        private Customer customer;
    
        public CustomerApi(Customer customer) { this.customer = customer; }
    
        @Path("accounts")
        public AccountsApi accounts() {
            return new AccountsApi(customer);
        }
    }
  6. Expose Domain Models via JAX-RS REST APIs

    main

    The domain model can be exposed via REST APIs by mapping the root association to a JAX-RS root resource and mapping entities and association objects to sub-resources.

    • Root Resource: Implement the root association (e.g., Customers) as the entry point (e.g., @Path("/customers")).
    • Sub-resources: Entities (e.g., CustomerApi) and association objects (e.g., SourceEvidencesApi) should be implemented as sub-resources to maintain the hierarchical structure of the domain model in the URI space.
    @Path("/customers")
    public class CustomersApi {
        private Customers customers;
    
        @Inject
        public CustomersApi(Customers customers) { this.customers = customers; }
    
        @Path("{id}")
        public CustomerApi findById(@PathParam("id") String id) {
            return customers.findById(id).map(CustomerApi::new).orElse(null);
        }
    }
    
    public class CustomerApi {
        private Customer customer;
    
        @Path("accounts")
        public AccountsApi accounts() {
            return new AccountsApi(customer);
        }
    }
  7. Implement Association Lifecycles (Aggregated vs Reference)

    main

    When implementing association objects, choose the lifecycle based on the expected data volume:

    Aggregated Lifecycle (In-Memory)

    Use this when the child entities should be loaded whenever the parent is loaded. Implementation: Extend reengineering.ddd.mybatis.memory.EntityList.

    import reengineering.ddd.mybatis.memory.EntityList;
    
    public class SourceEvidenceTransactions extends EntityList<String, Transaction> implements SourceEvidence.Transactions {
    }

    Reference Lifecycle (Database-backed)

    Use this for large collections to avoid loading massive amounts of data into memory at once. Implementation: Extend reengineering.ddd.mybatis.database.EntityList.

    import reengineering.ddd.mybatis.database.EntityList;
    
    public class AccountTransactions extends EntityList<String, Transaction> implements Account.Transactions {
    }