For a small nightly batch tool over one record shape, what argues for plain subroutines instead of an object model?
answer
- which axis actually varies here
- one shape, many operations
- indirection charged on every read
- a branch on kind in every routine
- pass a routine for one variation point
basics
~20 sPlain subroutines win where the record shape is fixed and shared, variation points are few, and the whole run reads top to bottom. An object model charges indirection for substitutability a one-shape batch tool never exercises.
solid answer
~50 sThe deciding question is which axis varies. A nightly batch tool over **one** record shape has a fixed data layout and a growing list of operations over it — totals today, a new deduction next quarter — and that is the shape procedural code is cheapest at: a new operation is a new routine and touches nothing else. An object model is cheapest at the opposite shape, where the operation set is stable and the **variants** multiply, because a new variant is a new implementation and existing call sites are untouched. A small batch tool also pays the object model's running costs without collecting its benefit: a call graph you must follow through dispatch to read, indirection whose payoff is substitutability the tool never exercises, and construction ceremony around a process that starts, makes one pass and exits. The answer flips the moment a second and third record kind appear and every routine grows a branch on kind.
code
pseudocode · 9 lines// data plus routines: a new operation is a new routine, nothing else changes
function netPay(record)
return gross(record) - deductions(record)
// but a new record kind is a branch added in every routine like this one
function gross(record)
if record.kind = "salaried" return record.annual / 12
if record.kind = "hourly" return record.hours * record.rate
// a third kind edits here, and in deductions, and in formatSlipgo deeper
Be able to say that a small program doing one job over one kind of record can be a set of routines, and that structure exists to serve a reader rather than to satisfy a convention.
Frame it as which axis varies: operations grow cheaply over plain data plus routines, variants grow cheaply in an object model, and a one-shape batch tool grows along the first.
Argue both directions with a concrete signal for switching — the same branch on record kind appearing in a third routine — and raise passing a routine as a parameter as the right-sized middle option.
Own the criterion rather than the verdict: state for your organisation when a tool graduates to a variant-oriented model, so the choice is a written trigger instead of the preference of whoever happens to write the next tool.
## Ask which axis varies This is the question the summary of this leaf points at — where does procedural structure still beat an object model — and the honest answer is not a preference but a shape test. Any program grows along two axes: - **Operations**: the things you do to the data (compute gross, compute deductions, format a slip). - **Variants**: the kinds of data you do them to (salaried, hourly, contractor). A layout of plain data plus routines over it makes **adding an operation** a one-place change — write a new routine, existing code untouched — and makes **adding a variant** expensive, because every routine that examines the kind grows a branch. An object model reverses exactly this: a new variant is a new implementation that existing call sites never see, and a new operation must be added to every implementation. Neither is better; they are cheap on opposite axes. A nightly batch tool over one record shape sits squarely on the operations axis. The record layout is imposed by the upstream file and changes rarely; the list of things you compute from it grows steadily. That is the case procedural structure is designed for. ## What the object model charges when you do not need it | Concern | Plain subroutines over shared records | An object model | |---|---|---| | Add an operation | one new routine | edit every implementation | | Add a record kind | a branch in every routine that cares | one new implementation | | Reading the run | top to bottom in call order | follow dispatch to find what actually runs | | Swapping behaviour at run time | an explicit branch or a passed-in routine | built in | | Test setup | construct a record, call a routine | construct the graph the object needs | The row that matters most for a small tool is **reading**. Indirection is not free to a reader: every dispatch point is a place where the code you are reading is not the code that will run, and you pay that cost on every visit whether or not more than one implementation ever exists. A batch tool is read far more often by someone diagnosing last night's run than it is extended, so legibility outranks extensibility in its cost model. ## Other honest arguments for the procedural shape here - **Process lifetime.** The tool starts, makes one pass and exits. Long-lived object graphs, lifecycle management and identity earn their keep in a resident system; in a one-pass job they are setup with no payoff. - **The data is genuinely shared.** All the routines read the same records. Splitting that record into objects that each guard a slice adds boundaries where the problem has none. - **Bulk shape.** Batch work is a loop over many records. A routine that takes the whole collection can process it in one pass; an object model usually pushes you toward per-record behaviour, which is not wrong but does not match the grain of the work. ## When the answer flips Be explicit about this in an interview, because the one-sided answer is the weak one. Switch when: 1. **A second and third record kind appear** and the same branch on kind shows up in several routines. That repeated branch is the signal the variant axis has become the growing one. 2. **Behaviour must be selected at run time** — by configuration, by customer, by region — rather than by a fixed branch the reader can see. 3. **The shared data stops being uniformly shared**, so that some routines need an invariant maintained across writes that others must not be able to break. 4. **The tool stops being small.** Legibility-by-reading-top-to-bottom is an argument that expires with size; at some point the call graph is too large to hold anyway and named boundaries start paying. And note the middle option a good candidate raises unprompted: you can pass a routine as a parameter to vary one step without adopting a whole object model. That gets you the one variation point you actually need at the cost of one argument, which for a small batch tool is usually the right-sized answer.
- What is the first concrete signal that the batch tool should move to an object model?The same branch on record kind appearing in several routines. One branch is a detail; the third copy means the variant axis has become the one that grows, and each new kind now costs an edit in every routine rather than one addition. That repetition, not program size or taste, is the signal worth acting on.
- The tool needs one step to vary per customer. Does that alone justify an object model?Usually not. Passing the varying step in as a routine parameter buys exactly one variation point for the cost of one argument, and leaves the rest of the run readable top to bottom. Reach for a full object model when several operations must vary together per variant, so that they need a common owner rather than separate parameters.
- A reviewer says procedural code here cannot be unit tested. How do you answer?A routine over a record is the easiest thing there is to test: construct a record, call it, check the result — no graph to assemble and nothing to substitute. What is genuinely hard to test is module-level state shared between routines, and that is a separate defect with its own fix, not a property of procedural structure.
saying these in an interview costs you the question
- Says an object model is always the more maintainable choice
- Claims procedural routines cannot be unit tested in isolation
- Believes indirection costs nothing because it compiles away
- Says adding a new record kind is equally cheap in both layouts
- Treats program size and lifetime as irrelevant to the choice