Web frameworks continue to evolve towards an ideal of highly automated view / presentation logic for Rich Internet Applications. ORM products are now an accepted and standards-based approach for persistence.
This places increasing attention on the approach and effort for Shared Transaction Business Logic, which can represent 50% of a typical update-oriented application. Though the term Business Logic
is often used, there is considerable confusion, ranging from definition to architectural approaches to appropriate tools.
This paper explores considerations and approaches for this important topic. Please see here
for a description of the ABL architecture.
This paper defines Transaction Business Logic as the
multi-table derivation, constraint and action logic required for database
integrity. We propose key criteria for evaluating
various architectural alternatives: encapsulation for active enforcement of logic, separation
to maximize interoperation with other components, performance, and business
considerations of Agility and TCO.
After surveying various alternatives, we propose Rich Active Declarative
Domain Objects, where a Business Logic Engine plugs in to ORM events for active
enforcement of declarative, multi-table logic.
What is Transaction Business Logic
Business Logic is a term often used informally,
but formal definitions have proved elusive.
So, it is appropriate to begin by defining what we mean, with an
Transaction Business Logic addresses only save (transaction) processing.
It complements other familiar forms of business logic such as
presentation logic (data retrieval / update, screen flow), Business Process
Flow, and Decision Logic commonly associated with Rete
Transaction Business Logic is most easily described in the context of this
familiar and representative example, where our logic specification for placing and modifying orders can be
customer.balance < customer.creditLimit
= sum(orders.total where paid =
= quantity * partPrice
Business logic elements include:
These are the most obvious elements of business
logic - expressions that prevent bad data from being committed into the
database. These include single value
constraints (not null, in value list, in range etc), and multi-value
constraints (as in the Constraint example above the diagram). Finally, it is often required that the expressions deal with old values (reject if salary < @old(salary) -- often referred to as state transition logic).
The most challenging element is derivations, since
these are typically chained (dependencies require detection and properly
ordered change propagation), and multi-table (so are performance-sensitive). In our example, the Customer.balance is multi-table (it sums that customer's unpaid
order.total), and is chained (since
the order.total is itself a derivation).
Actions include auditing, copying,
messaging, business process initiation, etc. Ideally, these are generic,
so they can be re-used over multiple Use Cases and Domain Models. For example, we might send email if a
Customers’ balance is close to the credit limit.
Such logic should be architected so that it encapsulates domain access,
automatically shared over User Interfaces, Message-based and all other types of transactions.
Why: it’s half the app
Transaction Business Logic is important since it constitutes
approximately half the code / effort for a typical transactional application.
Context: Well-Accepted Principles
We evaluate several alternatives for Transaction Business Logic
in the next section. Here, we define evaluation criteria based on several well-accepted architectural principles.
– guaranteed re-use, active enforcement
Encapsulation means all the
invoking code is guaranteed to utilize the same logic – various screens,
messages, etc. Not only does this mean
transaction business logic should not be placed in individual controllers, it
also means the job of the architecture is to assure the use of the logic.
In particular, enforcement ought
not to require the caller to make “magic calls” for logic invocation. The architecture should provide automatic, active enforcement of
Separation – snap-in components
Architectural separation means adding
additional services so they interoperate with, and minimize impacts on, existing
layers. For example, in a Java stack:
Interoperation: Using existing CRUD (Create, Read, Update, Delete) APIs,
for example SQL or ORM, means that existing tools / frameworks do not require custom code for interfaces, or to participate naturally in
building RESTful services
Interference: Domain Objects are widely used for persistence, transport,
etc. Injecting code into such objects
can affect characteristics such as serializability
is critical (pruning, adjustment)
As our example illustrates,
aggregates are integral to business logic.
Unlike simple attribute validations, such multi-table logic means that
minimizing SQL access plays a critical role in performance. It comes down to when and how the
aggregates are derived.
When means analyzing transactions to identify when there are no
changes to data on which the aggregate is dependent, and pruning SQL overhead. For example, changing an order’s date does not affect the customer’s balance, so no SQL overhead should be incurred.
Once the logic determines that
aggregates are affected, there are important strategies regarding how aggregate derivations are performed.
Application architects have long
recognized that performance denormalizations
(storing aggregate results in the parent object, such as the customer balance)
can dramatically improve performance by eliminating SQL aggregate queries, or
reading all the child rows into memory.
Such techniques enable the application to utilize adjustment logic, so, for example, changing a Line Item quantity
simply adds the delta to the Order total, which in turn adds the delta to
the Customer’s balance.
This is important when there are many
child rows per parent (orders per customer), and particularly important when
there are chained multi-level aggregates.
For example, changing a Line Item quantity should not entail adding up
all the Orders / Line Items to compute the balance.
denormalizations are a tuning parameter that can be changed as performance
characteristics become clear, without
affecting existing logic. A good
example of this is a relational database index: you can add it at any time
Agility and TCO – Declarative wins
The purpose of a strong
architecture is ultimately to provide business value. So, it is appropriate to address that issue
directly. Business certainly has an
obvious interest in the items above, but there are always two concerns near the
top of the list:
- Agility: business advantage goes to the organizations that are the fastest to respond
to new opportunities, or to change
Cost of Ownership: there is obvious benefit when maintenance costs are
reduced, and when transparency increases the communication between business and
IT to reduce requirements risk. There is
nothing as expensive as a system that fails to meet user needs.
In system terms, this pretty much
boils down to the volume and complexity of the code we write.
Core of the problem: Dependency Explosion
While there are Rete / Process engines for different aspects of Business Logic, the presumption is that
Transaction Business Logic is pretty much ad hoc code. The problem is, there’s a lot
Our simple exercise above, believe it or not, results in about 500 lines
The problem boils down to coding an
explosion of dependencies. For example,
our derivation for Customer.balance is directly dependent on
- Order.customer (the foreign key)
And, we are far from done, because
dependencies can cascade due to chained derivation logic. So, Customer.balance is also dependent on
everything Order.total depends on
Not done yet. Lineitem.amount is dependent upon
- Lineitem.part (the foreign key)
So, to reduce code volume /
complexity, our approach had better provide automated dependency management. This directly affects Agility (how long it takes to build the code) and TCO (since maintenance costs are substantial, and dominated by dependency-based ordering).
Declarative Approaches gaining acceptance: JSR 303, Grails, …
Declarative approaches are generally defined as those where you state what should happen, not how. For our purposes, the what is the 5 rules at the top. The how is the dependency automation, as well as other elements such as performance and re-use.
Declarative approaches are gaining increasing favor, due to the business value of the automation they confer. JSR 303 provides for Bean Validation, provided by various implementations such as Hibernate/JPA and Grails. While these provide minimal or no support for multi-table derivations, they do suggest an approach for specifying business logic.
Ideal: executable design (intent)
The ideal here is that the business logic is
- Concise – the ultimate goal is an Executable Design Specification, where the smallest list of formal specifications runs
- Transparent – the logic should also be clear enough so Business Users can read it, and collaborate with IT to spot errors, omissions, etc. This can reduce Requirements Risk.
The “rules” at the top of the page meet these requirements.
Simpler – fewer objects / less code – is better
Finally, to include a more
intuitive criterion, we suggest that simplicity is a virtue that has economic
value. In the context of Java, it is not
uncommon to require a Domain Object, a DTO (Data Transfer Object), a DAO
(Data Access Object), and Service Objects (for multiple table transaction
logic). These must be maintained and
kept in sync, reducing Agility and increasing TCO.
Alternatives for How
Using the criteria above, the sub-sections below briefly
survey alternatives for transaction business logic.
Rete-based inference engines
provide valuable services that enable Business Users to manage (safely selected) elements of their system, bypassing the need to contact IT. While valuable in that sense, Rete engines are quite inappropriate for the transaction processing elements of Business Logic:
- Poor aggregate performance
Aggregate processing is a well-known challenge for Rete engines. The root cause is that Rete engine interfaces do not presume a transaction that can be compared to an existing database to compute adjustments, and use these to prune processing logic. So, to define a sum, they must bring all the child rows into memory. Particularly when such aggregates are Forward Chained on other aggregates, this can be prohibitively expensive.
Unlike automated Business Logic where rules are automatically invoked for all updates, Rete rules require explicit calls. Integrity that is elective is not reliable, and does not meet the requirements of regulatory compliance or system integrity.
Rete engine logic must be explicitly called, which means extra code is required for frameworks that otherwise automate standard SQL/ORM persistence.
- Inadequate Expressive Power
Lacking persistence integration, Rete engines do not have concepts like old values, so there is no natural way to express state transition logic. They also do not provide advanced logic such as copy and allocation.
Process Engines provide excellent value in workflow and system integration processes, where a sequence of steps is inherently manually ordered. They are a poor fit for transactional Business Logic:
- Agility / TCO compromised – no dependency-based automated ordering
BPM ordering does not address the important dependency-based automated ordering, a core issue for transaction logic
BPM engines focus on workflow – they are not designed to provide persistence integration with pruning and adjustment logic that are crucial for multi-table transactions.
- No Encapsulation / poor separation
As described above
Visual Rule Designers
Visual Rule Designers provide a graphical (flow-chart like) way to program your business logic. While visually appealing, these are really totally procedural, suffering from all the liabilities of programming:
- Agility / TCO compromised: your diagram is inherently ordered, which must be designed – and re-designed – as changes are necessary
- No Encapsulation: your logic is not called without explicit invocation, so re-use is not guaranteed. Worse, re-use is of the procedural variety, not re-use of declarative logic (such as a sum, which is automatically re-used over inserts, updates and changes)
- Poor Separation: substantial manual code is required to invoke the logic
- Performance not automated: such engines typically require you to manually invoke SQL calls. This is a very low level of abstraction and obviates the possibility of automated optimizations
- Optimization: such procedural approaches require you to manually code optimization techniques such as pruning or adjustment, which must be reconsidered on each maintenance change
Database triggers provide excellent encapsulation. However, they incur all the disadvantages of manual dependency management, implemented in a non-standard programming language often without debugging tools.
Technologies such as Spring provide injection to intercept designated calls, such as JDBC/ODBC calls. This can provide encapsulation. But these are typically not integrated with ORM services for caching, so performance suffers.
Service Layer (multi-table, discoverable)
A very common approach to business logic is to build a Services Layer. Often designed to resolve complex design issues (is aggregate adjustment a responsibility of the child Order, or the parent Customer?), this involves creating explicit APIs for each Use Case (e.g., addOrder, deleteOrder, modifyOrder etc).
These clearly “publish” the Use Cases and public interfaces for the system. However:
- Agility / TCO compromised: Service Layers tend to be large. For example, the Service Layer for our simple example required around 500 lines of Java to deal with all the related Use Cases. In particular, the main source of code was extensive Dependency Management as described in Dependency Explosion, above.
- No Encapsulation: your logic is not called without explicit invocation, so re-use is not guaranteed.
- Poor Separation: substantial manual code is required to invoke the service logic, compromising framework SQL/ORMs automation
- Performance not automated: your code is responsible for optimizations described above such as pruning and adjustment. These can dramatically increase maintenance costs, since these must be reconsidered for each change. As with SQL indices, organizations ought to be free to introduce / remove performance denormalizations without rewriting.
Rich Domain Model: ORM Domain Objects as Data Access Objects
Domain layer proponents regard a Fat Service Layer
to be an anti-pattern (“Anemic Data Objects”
), since business logic is not encapsulated. It is often proposed that the Domain Objects be extended with Data Access Object “crud” methods. While this provides encapsulation, there are many issues not addressed:
- Architectural separation is not provided, since logic may interfere with the use of Domain Objects as transfer objects, and framework automation of existing persistence APIs is unavailable since these bypass the Domain logic methods
- Active, automated enforcement is not provided since direct calls to ORM updates (e.g., by frameworks) bypass Domain logic methods
- Business Agility is not provided since such Domain methods must be manually coded, and directly deal with the Dependency Explosion, optimization, and multi-object logic
Rich Active Domain Model (via ORM Events)
An elegant approach for addressing these concerns is to leverage ORM events for transaction business logic. This provides:
- Encapsulation of logic: any Domain Object update invokes the logic
- Excellent separation: frameworks that automate ORM calls can be used directly
This approach is so attractive that proponents have characterized the Fat Services Layer as an anti-pattern (so-called Anemic Domain Objects
), as contrasted to a Rich
Data Model that encapsulates business logic. ORM events confer a Rich Data Model with the benefits noted above, but there are drawbacks:
- No Performance automation: pruning, adjustment etc are still manual coding (and re-coding) exercises
- Poor Agility / TCO: the event handlers still must provide the dependency management to analyze what has changed, and recompute dependent derivations (which can, as we saw above, chain)
- Moreover, experience has shown that the subtleties of ORM event technology make even the simplest logic (e.g., auditing) very tricky
Rich Active Declarative Domain Model
We can now summarize how Automated Business Logic results in
Active Domain Objects can fit into your overall architecture using a Rich Domain Model approach:
by Event-based Injection, and
for Active Enforcement
Logic Engine automates multi-table business logic
These are further developed in the
Event-based Injection: Separation per use of existing APIs
While injection is usually associated with compile-time injection of logic, you get the same effect by plugging into ORM events. This provides the Architectural Separation:
- Maximize Interoperation: frameworks and tools that automate calls to ORM events can be fully leveraged, without any custom code to invoke services
- Minimize Interference: no impacts to Domain Objects (e.g., serializability for use as Transfer Objects)
So, for example, MVC-oriented interactive apps simply make the usual calls to persist data, and logic execution is automatic.
Encapsulation: automated re-use, active rule enforcement
Encapsulation of logic is conferred since any ORM updates automatically invoke the relevant logic. While encapsulation is traditionally implemented by Object technology, we achieve the same re-use using ORM Event Injection.
Note that re-use is automated. This is in sharp contrast to traditional approaches, which achieve re-use only by careful object design.
Note we are defining re-use in the broadest possible manner, where it applies regardless of transaction type (interactive or message), or (per automated dependency management) Use Case.
Declarative Logic Engine: Agility, TCO
The Logic Engine is driven by declarative rules, such as those noted in the example above. This reveals the fundamental advantage: you replace 500 lines of code with 5 rules. This can confer strategic business advantage.
Logic can be presented in multiple ways:
- Classes (using annotations, methods, DSL grammars, etc)
- Database rows (enabling runtime logic changes)
Regardless of how the logic is specified, the responsibilities of the Logic Engine are described in the sub-sections below.
Dependency Management: ordering, optimizations
Declarative means the system assumes responsibility for Dependency Management
- Change Propagation – with Forward Chaining
Dependency Management applies both initially, and when the logic is changed. This brings automatin advantages to maintenance, not just development.
Automated Dependency Management takes re-use to an entirely new level, moving beyond re-use ot code to re-use of intent. So, for example, the rules shown at the top of this page are automatically re-used over multiple Use Cases
: new order, delete order, re-assign order to different customer, etc.
Full Support for constrained multi-table derivations
The set of Business Logic constructs includes all the items noted above, particularly including optimized multi-table derivations.
ORM Integration: state transitions, pruning, cache utilization
ORM integration means that the Business Logic Engine has access to both the transaction changes, and the original data. This enables the engine to make the original data available (e.g., salary < @old(salary)) for state transition logic, and to perform intelligent pruning of rules whose dependent data has not changed.
Extensible (Action rules)
Ideal implementations provide extensibility at two levels:
- Ad hoc: Developers will always need to provide procedural logic for the exceptions, with full access to existing libraries
- Generic: Systems Developers should be enabled to provide new reusable rule types, enabling them to extend the services of the Business Logic Engine.
Optional Thin Service Layer (e.g., to publish APIs)
There is a fair bit of confusion in the industry regarding where to put business logic: in the domain objects, or in a service layer. Domain layer proponents regard Fat Services with Anemic Data Objects to be an anti-pattern, since they don't encapsulate business logic. Service layer proponents express concerns about how to embed multi-table logic - is the adjustment of a Customers' balance a responsibility of the Purchase Order, the Customer, or some mediator object?
Rich Active Declarative Domain Objects not only provide automation, they can dramatically simplify this quandary. Multi-table logic is automated - from the "what" based design objective - including its implementation design. That is what we mean by Rich Active Declarative Domain Objects - domain objects that actively enforce your logic by acting in concert with related objects on the basis of declarative rules.
So, for the vast majority of your transactions, Rich Active Declarative Domain Objects address the multi-table requirements (and complexity) traditionally associated with a Service Layer. You may still elect to provide a Service Layer, for example to publicize APIs, or to address transactions that are not fully REST-based. But even in these cases, you will find that the Service Layer will be thin: it needs only to choreograph updates to the Domain Layer, since it can be assured these will enforce the multi-table business logic.
This paper has presented a Rich Active Declarative Domain Model architecture for Transaction Business Logic, providing excellent architectural separation, active encapsulation of logic, enterprise-class performance, and strong business value in Agility and TCO. These concepts can be applied to a number of different platforms: Java vs. .NET, various client topologies, and message-based transactions.