skip to content

What is the Table Module pattern for organizing domain logic, and in what situations does it fit better than Transaction Script or Domain Model?

level: middleimportance: should knowfreq 30%

answer

  1. one class per database table, not per row
  2. methods operate on whole record sets
  3. fits record-set-rich tooling (ADO.NET DataSet)
  4. no per-row object identity
  5. middle ground between Transaction Script and Domain Model

basics

~20 s

Table Module has one class per database table, not per row, and it holds the business logic for that table. You pass it a set of rows, like a whole table's worth of data, instead of creating a separate object for each individual record.

solid answer

~40 s

Table Module creates a single class per database table or view that wraps a record set (like a DataTable or ResultSet) and provides business methods that operate on rows within it, instead of instantiating one domain object per row the way Domain Model does. It fits well when the application's tooling gives you a rich in-memory record-set abstraction, like .NET's ADO.NET DataSet, so you avoid the overhead of hydrating many small per-row objects. It's less natural in ecosystems without strong record-set support, such as plain Java or Kotlin, where Domain Model or Transaction Script over DTOs is more idiomatic. It sits between the two other patterns: more organized than a single monolithic Transaction Script, but without the per-instance identity and polymorphism of a full Domain Model.

go deeper

for a junior

Should recognize that Table Module operates on a whole table or record set rather than one row-object, even without deep tooling context.

for a middle

Should name a platform where it fits well (record-set-rich tooling like ADO.NET DataSet) and contrast it with per-instance Domain Model objects.

for a senior

Should explain why it's less common in modern mainstream OO ecosystems and articulate its limitations around per-row identity and polymorphism.

for a principal

Should be able to judge whether legacy Table Module code justifies migrating to a Domain Model given current ORM tooling maturity, weighing migration cost against the actual benefit.

## What Table Module is Table Module, one of Martin Fowler's three Patterns of Enterprise Application Architecture domain-logic patterns, organizes business logic **by database table** rather than by transaction or by domain concept instance. You create one class per table or view — say an `OrderTable` class — and that class's methods take a row or, more commonly, a whole in-memory record set as a parameter, and return computed results. A method like `CalculateTotalsFor(DataTable rows)` operates across an entire batch of order rows at once, rather than a Domain Model's approach of instantiating a separate Order object with its own identity for every row. This works hand in hand with a **Recordset** construct that behaves like an in-memory table — rows and columns, sortable and filterable — often produced directly by a SQL query, so there is almost no translation gap between what the database returns and what the business logic consumes. ## Why the pattern exists The pattern exists because certain platforms hand you a rich, navigable in-memory table structure essentially for free. The motivating case, and the one Fowler names explicitly, is Microsoft's COM and .NET tooling: ADO.NET's `DataSet` and `DataTable` give you sorting, filtering, relations, and data-binding to UI grids without writing any mapping code. Building a full Domain Model on top of that means paying an object-relational mapping cost the platform doesn't actually require, since the record set already is a convenient in-memory table. Table Module lets you attach business behavior directly onto that structure, keeping the platform's convenience while still giving business logic an organized home — grouped by table concept and reusable across multiple use cases — rather than scattering it across many one-off Transaction Scripts. ## The trade-off — convenience against weaker object semantics The trade-off is convenience and low mapping overhead in exchange for weaker object semantics. **On the plus side:** - no ORM cost on record-set-native platforms, - logic organized and reusable per table concept rather than duplicated per transaction, - and natural fit with tools that bind UI grids or reports directly to the record set. **On the minus side:** - there's no true object identity per row, so modeling polymorphic per-instance behavior — different order types behaving differently, for example — is awkward, since you'd need to branch on a type column inside the table class rather than override a method on a per-row object; - it's also harder to unit test in a fully isolated way compared to plain objects, since you're constructing and manipulating loosely typed rows and columns rather than calling typed methods on a domain instance; - and outside record-set-rich platforms, most mainstream OO languages — Java, Kotlin, modern C# with Entity Framework — don't have a first-class in-memory table type to hang Table Module methods off of, which is why the pattern is comparatively rare today outside some enterprise .NET and legacy VB6/classic-ASP.NET systems. ## How it fails A common failure mode is teams hand-rolling a fake record-set abstraction in a language that has no native one, adding real complexity for a benefit the platform was never going to give them in the first place. The opposite failure is sticking with heavy DataSet-driven Table Module code in modern .NET long after Entity Framework — a proper ORM enabling Domain Model — became the standard, well-supported approach; this legacy style tends to accumulate loosely typed row access like `row["ColumnName"]`, which causes runtime type errors and typo bugs that a domain object with strongly typed properties would have caught at compile time, and it's noticeably harder to unit test without either a database or a hand-built fake record set. ## Where it shows up, and how it sits between the other two In practice, Table Module shows up in classic ADO.NET, VB6, and classic ASP.NET WebForms applications built around DataSet and DataAdapter, where business logic classes operated directly on typed DataTables bound to UI grids. Positioned against the other two Domain Logic Patterns, it's a genuine middle ground: | Pattern | Where it sits | |---|---| | Transaction Script | the simplest, with no shared structure at all | | Table Module | adds organization by table while staying row-set-oriented with no per-row objects | | Domain Model | the richest, with per-instance objects, identity, and behavior, at the highest mapping and learning-curve cost | Recognizing which of the three a legacy codebase is using — and why it was a reasonable choice for its era's tooling — is often the first step in deciding whether a migration to Domain Model is actually worth the cost today.

  • Why did Table Module become less common in modern Java, Kotlin, and C# backends?
    Mainstream ORMs like Hibernate, JPA implementations, and Entity Framework Core made Domain Model and Data Mapper cheap enough that the mapping overhead Table Module was specifically avoiding largely disappeared, and most OO languages never had a native, rich record-set type to hang Table Module's behavior off of the way .NET's DataSet did.
  • Can Table Module logic be unit tested without hitting a real database?
    Yes, if you construct the in-memory record set directly with test data rather than querying it live — but it's typically less ergonomic than instantiating a plain domain object, since you're manipulating loosely typed rows and columns instead of calling typed methods and asserting on typed properties.

Like a spreadsheet macro that operates on an entire sheet at once, rather than either following a one-off checklist per entry (Transaction Script) or treating each row as its own little smart object (Domain Model).

saying these in an interview costs you the question

  • confuses Table Module with an ORM entity representing one row
  • says it requires no notion of an in-memory record set or table structure
  • doesn't know it groups logic by table, not by business transaction
  • claims it's identical to Domain Model
  • unaware that it lacks per-row object identity

context