The idea: one rule, three places to put it

Every business application has domain logic: the rules of the business, apart from screens and storage. Martin Fowler's Patterns of Enterprise Application Architecture names three main ways to organize it, and runs one small example through all three: revenue recognition, the question of when money from a signed contract may be counted on the books. A company sells three products. A word processor books all its revenue on the day the contract is signed. A spreadsheet books one third on signing, one third at day 60 and one third at day 90. A database books one third on signing, one at day 30 and one at day 60. The page codes these rules three times and runs each contract through all three at once, so you can see where the rule lives, what is touched, which SQL runs and which rows come out.

Three call diagrams for calculating one contract. Transaction Script: one RecognitionService procedure calls a gateway to find the contract and inserts each recognition. Table Module: Contract, Product and RevenueRecognition modules share one record set; Contract asks Product for the type and calls Insert. Domain Model: a Contract object calls its Product, which calls its RecognitionStrategy, which creates RevenueRecognition objects.
The same calculation, three shapes: one procedure, modules around a record set, and a chain of objects ending in a strategy.

The database has three tables: products (ID, name, type), contracts (ID, product, revenue, dateSigned) and revenueRecognitions (contract, amount, recognizedOn) with the primary key (contract, recognizedOn). If a contract has recognitions, they must add up to its revenue. Each style writes its own copy of the recognitions table, so the three results can be compared row by row.

Transaction Script

A Transaction Script is one procedure per thing a user can do. calculateRevenueRecognitions(id) reads the contract and its product through a thin Table Data Gateway, tests the product type in an if chain, splits the money and inserts the rows. Most developers can read it at once, and the transaction boundary is obvious: begin at the top, commit at the end. Scripts on one subject usually share a class; Fowler also shows each script as its own command object (see the Command pattern). Demo 1: one contract, three ways shows it reading one joined row and sending one INSERT per recognition straight away.

Table Module

A Table Module is one class per table with one instance for all its rows. The client first fills a record set (a .NET DataSet) with queries, then hands it to Contract, Product and RevenueRecognition modules. Because an instance stands for the whole table, every call takes an ID: contract.CalculateRecognitions(102), product.GetProductType(2). The rows are added to the DataTable in memory and written by an adapter when the record set is saved. It fits platforms whose screens and tools are built around record sets. Logic grouped by table is easier to search for duplicates than a pile of scripts, but there is no object per row, so the type is still picked by a switch.

Domain Model

A Domain Model has one object per row that holds both data and behaviour. Contract.calculateRecognitions() asks its Product, which passes the job to its RecognitionStrategy: a CompleteRecognitionStrategy for word processors, a ThreeWayRecognitionStrategy(60, 90) for spreadsheets and (30, 60) for databases. No conditional runs during the calculation; the choice was made when the product was wired to its strategy. Getting objects in and out of tables needs a Data Mapper and a Unit of Work, which the book leaves out of its example. The page adds a small ProductMapper registry from type code to strategy, marked as its own addition. A simple domain model that mirrors the tables can use Active Record; one with strategies and inheritance needs a Data Mapper. See also Strategy and Factory Method.

Transaction ScriptTable ModuleDomain Model
organized byuser actiontablebusiness concept
instancesnone neededone per tableone per row
picks the rule byif chainswitch on an enumthe product's strategy object
SQL for one calculation1 + n (INSERTs at once)3 + n (INSERTs on save)2 + n (INSERTs on commit)
data source layerTable or Row Data Gatewayrecord setsActive Record or Data Mapper

When the rule is missing

Sales can sell a product before the code knows its rule. Demo 2: a product with no rule yet calculates contract 105 for Thinking Show, type P. The book's script has no final else, so it falls through and commits nothing, with no error: the 50.00 is never recognized and nobody is told. The table module ends its switch with throw "invalid product id". The domain model fails even earlier, when the mapper finds no strategy for type P. An error that stops the job is better than a silent nothing.

Adding rules: count the places

Make Next Change climbs a ladder of four change requests. For each style the page counts the places to edit, the copies of the type switch and the rules at risk: existing branches inside edited code, which must be tested again. C1 and C2 are the page's own inventions; C3 (one twelfth a month) and C4 (a signing-date cut-off) follow remarks in the book.

changeTransaction ScriptTable ModuleDomain Model
C1 new product Thinking Show: thirds at day 0, 45, 90+ branch in the script: 1 place, 3 at risk+ enum value, + branch: 2 places, 3 at risk+ registry entry, no new class: 1 place, 0 at risk
C2 second use case: quote preview+ a script with a copy of the if chain: 1, 0switch moved into Product.Schedule: 2, 4+ previewSchedule reusing the strategies: 1, 0
C3 support plan: 1/12 every 30 days+ branch in both copies: 2, 8+ enum value, + branch, one module: 2, 4+ MonthlyRecognitionStrategy: 2, 0
C4 spreadsheets signed from 15 April: day 0, 30, 60nested if in both copies: 2, 10nested if: 1, 5+ DatedRecognitionStrategy: 2, 1
total6 places, 21 at risk, 2 copies7 places, 16 at risk, 1 copy6 places, 1 at risk, 0 copies

The places come out about even. What separates the styles is where the edits land: the script and the table module keep editing code that already works, while the domain model mostly adds new classes beside the old ones. That is the open/closed principle from SOLID, and the copied switch is the kind of duplication Coupling and Cohesion counts. Demo 3: add rules shows the copy appear and the new monthly strategy; Demo 4: climb the whole ladder ends with the totals above.

Effort and complexity

Fowler sketches the trade-off as three curves of effort to enhance against complexity of the domain logic, and says himself that the axes have no units. With simple logic, the Transaction Script is cheapest and the Domain Model dearest: its objects take a while to learn and its mapping to tables is a fixed cost. As rules and use cases pile up, the script and then the table module hit a wall, while the domain model's effort grows slowly. A team that already knows domain models moves that curve down; good record-set tooling makes the table module more attractive. The styles mix: scripts for simple actions, a domain model for the hard part. Moving from scripts to a domain model is worth doing when the scripts crack; going the other way rarely pays.

A chart without numbers: effort to enhance on the vertical axis, complexity of domain logic on the horizontal one. The Transaction Script curve starts lowest and bends up steeply, the Table Module curve starts a little higher and bends less, the Domain Model line starts highest and rises slowly, crossing the other two as complexity grows.
The book's sense of the trade-off: cheap scripts for simple logic, a domain model once the logic gets complex. No units on either axis.

Splitting money

Dividing 100.00 by 3 gives 33.333…, and rounding each part gives 33.33 three times: 99.99, a cent lost. Dividing 200.00 gives 66.67 three times: 200.01, a cent made up. The book's Money.allocate works in whole cents and gives the leftover cents to the first parts: 33.34 + 33.33 + 33.33 and 66.67 + 66.67 + 66.66. The monthly plan splits 100.00 into four parts of 8.34 and eight of 8.33. Demo 5: losing a penny ticks round each part and shows the sum check fail in all three styles.

Service Layer

Domain logic is not all the logic. After the recognitions are calculated, the application must email the contract administrator and tell other systems through a message queue. Randy Stafford's chapter in the book calls this application logic, and puts it in a Service Layer: RecognitionService.calculateRevenueRecognitions(n) is the one entry point that a web UI, a data loader and an integration gateway all call. It reads the contract, lets the domain model do the domain logic, then sends the email and the message. It is also the transaction boundary: the method is declared transactional, so the saved rows, the email and the message go out together or not at all.

Three clients, a web UI, a data loader and an integration gateway, call one Service Layer operation. Inside one transaction it calls the domain model, then the email gateway and the integration gateway, and writes to the database. On commit the rows are saved, the email is sent and the message is delivered; on rollback all three are discarded.
One entry point for every client, and one transaction around the database, the email and the message.

Demo: three clients, one service runs one calculation from each client. Demo: commit fails, nothing leaks calculates contract 102 twice: the second commit breaks the primary key, and the held email and message are thrown away with the rows. Demo: no service layer gives each client its own copy of the coordination. The copies drift: the web UI sends its email before the commit, so a failed commit leaves a wrong notice, and the data loader's copy never got an email step at all. The service can be a thin domain facade that only forwards calls, or an operation script that holds the application logic, as here. Fowler prefers it thin, and adds it only when there is more than one kind of client or more than one transactional resource. Logic used by one use case only can live in a script; logic shared by many belongs in the domain. See also Hexagonal Architecture, where the use cases at the centre play the same role between the driving adapters and the domain.

What the page leaves out

A working Data Mapper and Unit of Work (the page shows only their SQL), typed DataSets, EJB session beans and container-managed distributed transactions, Remote Facades and Data Transfer Objects for remote calls, the controller-entity style, Active Record, real accounting rules, time zones and calendar months (the page adds whole days), and the book's C# allocation helper, which rounds differently from Money.allocate.

On the mind map

This page on the Software Design mind map · 28 pages in 7 branches · open it to see the whole map and test yourself in recall mode.