
[August 2026 edition]
The Craft of Splitting Things Apart
Published: Aug 20, 2026
Reading time: ~27 min
Building software means splitting things: a huge table into partitions, a mass of records into batches, a tangled business process into use cases, a single operation into synchronous and asynchronous halves. Even the continuous, shifting real world gets recorded as a finite set of states. Long before anyone writes code, design has already become a question of where to cut.
The premise that splitting makes things easier only holds inside the pieces. Every cut adds a seam, and seams need communication, joins, synchronisation, consistency checks, and error handling. When the complexity at the seams exceeds the complexity removed from inside, the system as a whole becomes harder to work with, not easier.
0. The Criterion, and the Four Things We Split
Before deciding where to cut, you need a basis for cutting. Without one, work gets divided along whatever boundaries happen to be visible, and the seams end up wherever chance puts them. This chapter sets out the criterion and the four kinds of thing it applies to.
Boundaries, Not Smaller Pieces
David Parnas’s 1972 paper proposed cutting modules not along the sequence of processing steps but so that each one hides a design decision likely to change. Splitting along the flow of processing looks natural, but when the specification changes, the effect reaches several modules at once. When a module hides a decision, changing that decision leaves everything outside it untouched. The same criterion survives today in microservices, domain-driven design, and data partitioning. The goal of splitting is not smaller pieces but boundaries within which you can decide, process, and change independently.
Information, Problem, Process, State
What gets split falls into four groups. Splitting information targets columns, records, time ranges, and keys, and limits how much any one operation reads and writes. Splitting the problem targets business processes, decisions, responsibilities, and rules, and keeps business logic at a size people can hold in their heads. Splitting the process targets steps, dependencies, and the synchronous/asynchronous divide, and returns results within a useful time. Splitting state targets real-world situations, observations, and time, and turns an analogue world into something a computer can act on.
The four are not independent. Once a designer decides where to split the business, the scope of the invariants that must hold together is fixed, and with it what can sit inside a single transaction. Once the transaction scope is fixed, anything updated together has to live in the same partition, which narrows the usable partition keys. Once the partition key is set, the unit of parallel execution matches the unit of partitioning. And once you decide where to break the processing, you have also decided what state each break must save and when a result becomes final. A designer therefore cannot settle the four separately. Decide how to split the business first, and the remaining three get decided from a narrower set of options.
The four decisions chain in this order.
From here the four are covered in turn, followed by the design work of putting the pieces back together.
1. Splitting Information — Limiting How Much Gets Read
Splitting information decides how much any one operation reads and writes. Columns, records, partitions, time windows — whichever you cut, the purpose is the same limit on scope. What determines the cut is not the shape of the data but the decision the data serves.
Projecting Attributes onto Decisions
Faced with data that has many columns, physically splitting the table first usually goes badly. The thing to look at first is which decision each attribute serves.
The concept of “customer” can hold a name, contact details, a billing address, a delivery address, credit information, purchase history, marketing consent, and support history. But shipping a product does not need the details of a credit assessment, and answering an enquiry does not need past payment tokens. Treat all of it as one enormous customer record and every operation ends up depending on more information than it needs.
Splitting starts by projecting the information a use case actually needs. Next, group the attributes that change for the same reason, separate those with different update frequencies or retention periods, and separate those with different access permissions. Only once the physical volume of reads becomes a problem is it time to optimise the storage format. Normalisation is a way of separating duplicated facts and independently changing facts, and for analytical work a columnar layout that scans only the needed columns pays off. Google’s Dremel combines a multi-level execution tree with a columnar layout to run aggregations over large nested datasets.
Treat logical and physical splitting as separate matters. One concept in the domain may take several shapes in read models. Conversely, columns can share a single table in storage without every operation needing to know about all of them.
Partition Keys and How Cost Gets Distributed
Large volumes of records are usually split by range, hash, value category, owner or tenant, time, or processing status. PostgreSQL’s table partitioning offers range, list, and hash methods, and the official documentation’s example puts a date in the partition key so that queries over recent periods and deletion of old ones both stay cheap.
A key that divides evenly is not necessarily a good partition key. What matters is that the main operations complete within one partition, that data updated together sits in the same partition, and that neither volume nor load skews badly. Being able to localise retention, deletion, and migration, keeping failures contained to some partitions, and leaving room to repartition later all belong on the list too.
Send two kinds of query at the same three partitions and the number of partitions read swaps around.
| Query | Split by customer ID | Split by date |
|---|---|---|
| One customer’s order history | 1 partition | all partitions |
| One day across the company | all partitions | 1 partition |
A partition key is not a database setting. It is a statement of business priority about which operations get to be cheap and which are allowed to be expensive.
Carving services along table boundaries goes wrong here. A table is a storage format, not a business capability. One business decision may use several tables, and one table may serve several read paths.
Counts Versus Meaningful Boundaries
“Process ten thousand at a time” and “page a hundred at a time” are easy to implement. But when the count is the only criterion, meaningful boundaries get lost. Split billing totals per customer, stock allocation per product, or contract validity checks into blocks of ten thousand, and any customer, product, or contract that straddles a chunk boundary forces you to recombine partial results afterwards.
Splitting with LIMIT and OFFSET also produces duplicates and gaps unless a stable, unique ordering is specified. The PostgreSQL documentation explains that without an ORDER BY that resolves uniquely, which rows come back is unpredictable, and that a large OFFSET is inefficient because the skipped rows still get computed on the server.
Bulk processing therefore uses meaningful boundaries rather than “rows N through M”. Conditions such as a primary key above the previous value, a target timestamp within a range, or a hash of the customer ID within a range keep any single contract or order from being cut in half. Treating events before a checkpoint as already processed belongs to the same family. Chunk size is not the split itself but an execution-level tuning value for how much of a unit to handle at once. “A thousand for now” is a performance setting, not a design rationale.
Cutting Endless Data by Time
An event stream has no clear end. Try to compute a total across all of it and there is no point at which waiting is finished. Apache Flink’s windowing divides an unbounded stream into finite buckets so that ranges of time or counts become computable.
Splitting here has two axes. One is the key: events belonging to the same customer, device, account, or order are gathered into the same processing unit. The other is time: five minutes, an hour, a business day, or the length of a session determines what counts as one aggregation target. Because the total ordering Kafka guarantees is essentially per-partition, a Kafka key is both a load-distribution setting and a statement about which events must keep their relative order.
Beyond that, three things need deciding: when the event actually happened, when the system received it, and how much has to arrive before a result is final. Whether to ignore late arrivals, correct past results, or wait for a fixed period is settled here. A window is a split by time and, at the same time, a business rule about when incomplete information counts as complete enough.
2. Splitting the Problem — Cutting by Decision
The unit for splitting business logic is not a screen, a table, or a CRUD operation, but a business decision. Take decisions as the unit and it becomes clear, one at a time, who decides what, on which inputs, under which rules, and what must always hold. A designer groups the decisions governed by the same rules to draw a boundary, and from the invariants that must hold inside it, determines what belongs in a single transaction.
What Is Not a Unit — Screens, Tables, CRUD
Before data or processing gets split, there is a stage of splitting the problem itself. Making screens, tables, or CRUD operations the unit of business splitting means nothing expresses what the business decides or guarantees. A customer screen exists, so a customer service gets built; an orders table exists, so an order class gets built, with create, update, and delete. Splitting purely by technical layer — Controller, Service, Repository — amounts to the same thing. If one use case cuts across many services that all share the same enormous data model, nothing has been split logically.
What to look at when splitting the problem is decisions rather than business nouns. Order handling breaks down into: may this order be accepted, may this product be sold, can the stock be reserved, can payment be taken, what delivery date can be promised, may shipping begin, may this be recognised as revenue. All of them concern “the order”, but they do not all belong to the same model, the same transaction, or the same moment.
For each decision, establish who decides, what the inputs are, which rules apply, what must always hold, which state changes, and what gets published externally. The deadline by which a decision is needed, and what happens when no decision can be made, are settled at the same stage. Cast this way, a vague “order management feature” turns into use cases that can actually be built.
Where the Boundary Goes — Vocabulary and Invariants
Domain-driven design’s Bounded Context divides a large model into several internally consistent models and makes their relationships explicit. The same words — “customer”, “product” — mean different things in sales, delivery, accounting, and support. Expressing an entire large business in one unified model is neither realistic nor economical.
Candidate boundaries are the places where the meaning of a word changes, where the responsible department or person changes, and where the applicable rules or the invariants that must hold immediately change. Places where freshness requirements change, where recovery procedures after a failure change, and where the reason for change itself changes belong on the same list. Decomposing services by subdomain likewise recommends taking business capabilities or subdomains as the unit and grouping functions that change for the same reason. Making services smaller on its own buys neither autonomy nor ease of change.
A Bounded Context does not have to become a microservice. What is needed first is a boundary in the code and the model; splitting processes or databases physically can wait until independent deployment, scaling, or fault isolation actually demands it. Split physically while the boundary is still unclear and function calls inside one process turn into network calls, while updates that fitted in one transaction turn into a distributed consistency problem.
The phrase “business logic” also mixes together processing of different kinds. Never allocating beyond available stock, never billing a cancelled contract, never letting an unauthorised user approve — these business decisions and invariants are domain logic. Taking input, fetching the target, invoking the domain decision, saving, publishing an event, and returning a result is application logic, the control flow. Connections to databases, message brokers, external APIs, and files are infrastructure logic. Hexagonal architecture separates the use cases inside an application from external technology and connects them through ports and adapters. Without that separation, the business decision of whether stock can be allocated ends up sharing space with SQL, HTTP calls, retries, and user-facing messages, so changing a business rule requires understanding the database and the transport as well.
Invariants and Transaction Boundaries
The highest priority when splitting is whatever must hold at the same instant: an account balance that never drops below the available amount, a seat never sold twice, the same stock never allocated to more than one order. In Life Beyond Distributed Transactions, Pat Helland treats an entity as a collection of data with a unique key that lives inside a single transaction scope while arguing for designs that do not rely on distributed transactions. Information that must stay strongly consistent belongs inside the same boundary where possible.
Compare the inside and the outside of a boundary in the case where payment fails after stock has been reserved.
Inside, the reservation and the order sit in the same transaction, so when the failure happens both revert automatically. Outside, the reservation mark stays on the stock service and does not go away until an undo is sent and its arrival has been reconciled.
Once processing crosses a boundary, nothing can be committed all at once. It becomes a design with provisional states, messages that start the next step, tolerance for duplicate execution, compensation on failure, and reconciliation afterwards. The Saga pattern divides a business process spanning several services into several local transactions plus compensating actions. It does not, however, give you automatic rollback or full isolation; compensation and conflict handling have to be designed explicitly. Splitting does not remove transactions. A single atomic transaction is decomposed into intermediate states, messages, retries, compensation, and reconciliation.
3. Splitting the Process — Returning a Result in Time
What splitting the process shortens is not the total amount of work but the time until a response. The only thing that comes off is waiting time on the longest chain of dependencies; work moved off that chain does not disappear, it keeps running somewhere else. The criterion for splitting is the business deadline, and that deadline gets distributed down to the steps below.
The Critical Path, and Working Back from a Deadline
What determines response time is not the number of steps but the length of the longest chain of dependencies. That means looking at processing as a chain of work that consumes time, rather than as functions in code.
Before adding threads or workers to make things faster, it pays to sort out the dependencies between steps. Order handling breaks down into validating the input, checking whether the customer may buy, checking whether the product may be sold, reserving stock, authorising payment, confirming the order, sending a confirmation email, and emitting an analytics event. The customer and product checks can potentially run in parallel, and stock reservation and payment authorisation can be parallelised as a provisional hold where the business allows it. Confirming the order cannot start until the necessary decisions are in, and the confirmation email and the analytics event can usually wait until after the response has gone out.
On that dependency graph, what determines response time is the longest path — the critical path. Parallelise nine of ten steps and if the remaining one accounts for most of the elapsed time, the response barely moves. The argument that the non-parallelisable part limits the achievable speedup is known from what Amdahl showed in 1967.
Start the three steps of order handling at the same moment, put time on the horizontal axis, and the relationship becomes visible.
All three run from the same start line, but the customer and product checks finish first while only the payment authorisation, which calls an external API, keeps extending. The response time stops when the last one finishes. Making the two fast ones faster does not move that position.
Deadlines come before speed in process design. Put real business deadlines in place — two seconds for a user-facing response, 500 milliseconds to reserve stock, six the next morning for a daily aggregation, before payment settles for fraud detection, five minutes for an email — and distribute the overall deadline across the steps. Where a step calls an external API, look at the slow tail of the distribution rather than the average alone. In a service that calls many downstream steps, delay in a few of them dominates the total wait. Google’s The Tail at Scale covers the effect of tail latency in large services and how to contain it.
The values worth watching per step are p50, p95, and p99, plus timeout rate, retry rate, queue wait, actual processing time, and time per external dependency. Response time is not determined by computation alone; it includes waiting, lock waits, connection waits, queue waits, and retry waits.
Moving Waiting Time by Going Asynchronous
Going asynchronous is not itself a speedup. All it shortens is work that does not have to be waited for in order to produce the current response. The test is narrow: does the result of that step change the content of the response about to be returned? If it does, it stays on the critical path as a rule; if it does not, it may be movable to a queue. Classified for order handling, it comes out like this.
| Step | Needed synchronously |
|---|---|
| Input format validation | Yes |
| May the product be sold | Yes |
| Can the stock be reserved | Usually yes |
| Was the payment accepted | Depends on the business model |
| Sending the confirmation email | Usually no |
| Updating analytics data | No |
| Updating the search index | Usually no |
Work moved to the asynchronous side brings new responsibilities. Messages must not be lost, duplicates and reordering must be handled, intermediate states must be explainable to users, failures must be detected with a bounded number of retries, and eventual consistency must be reconciled. The volume of work does not go down, so making every slow step asynchronous does not fix a shortage of capacity. The waiting time simply moves somewhere the user cannot see.
The Limits of a Queue, and What Happens Under Overload
Adding a queue does not increase how much work can be done either. What it adds is room to absorb temporary bursts and to reconcile differences in speed. If work keeps arriving faster than it can be processed, the queue grows without bound. Reactive Streams treats propagating backpressure across asynchronous boundaries — so a fast sender does not overwhelm a slow receiver and queues stay bounded — as its central problem.
A queue therefore needs limits on item count, total data volume, waiting time, attempts per item, total retry volume, and concurrency. What happens when a limit is exceeded has to be decided in advance: reject new work, drop low-priority work, fall back to a simplified path, slow the producer, expire work that has gone stale, or route it to a human. The cascading-failures chapter of Google’s SRE book likewise describes bounding queue length and concurrency under overload and using load shedding and graceful degradation to stop the whole system collapsing. If a user no longer needs a response they waited ten seconds for, finishing that work creates no value and delays the requests behind it.
Propagating Deadlines and Cancellation Downwards
When a downstream step keeps running after the upstream deadline has passed, it consumes resources for nothing. If the screen has a two-second budget and 1.5 seconds are already gone, the remaining budget for a downstream service is 0.5 seconds. gRPC deadlines let a client specify how long it will wait and propagate that deadline to the calls it makes. Not continuing unnecessary work past a deadline pays off in compute resources as well as latency.
Making work cancellable means splitting it at sensible interruption points: per item, per chunk, before and after an external call, before and after persistence, at stage boundaries, at checkpoints. Implemented as one enormous operation, there is no safe place to stop even once the deadline is known to have passed.
Update Operations That Survive Retransmission
In distributed processing a request can succeed while only the response is lost. The client cannot tell whether it worked and sends the same request again. Update operations therefore need to be idempotent: issue a unique idempotency key per operation, record the keys already handled, return the same result for the same key, avoid repeating side effects, and state when keys expire. The reliability pillar of AWS Well-Architected likewise recommends attaching idempotency tokens to mutating operations so that a request arriving several times can be retried without producing duplicate records or duplicate side effects.
Which Layer Owns Retries
When every layer retries on its own initiative, requests get amplified against a system that is already failing. If the client, the API, the service, the worker, and the database connection each retry, one failure turns into a flood. Which layer is responsible, how many attempts at most, which errors qualify, at what interval, and whether it all fits inside the overall deadline are settled as a single policy.
4. Splitting State — Reducing a Continuous World to Finite States
Reality changes continuously, and those changes reach the system late and incomplete. Splitting state is the work of converting that reality into finite states, events, and timestamps. What gets decided here is not the names of the states but which transitions are allowed, as of when a fact is recorded, and whether it can be corrected later.
Separating Business, Observed, and Processing State
The real world does not behave like a database. A parcel is not delivered the instant the system sets it to “delivered”: it changes hands first, then gets scanned on a device, then transmitted, then processed on a server. Equipment rarely flips from healthy to failed in one step either; temperature, vibration, voltage, and response time drift, and a failure is judged after several signals. A contract cannot be expressed as valid or invalid alone — it moves through applied, under review, approved, awaiting effect, active, being amended, scheduled for cancellation, ended, revoked, and unknown. Digitising reality is the work of converting a continuous, ambiguous world into finite states, events, numbers, and timestamps the system can act on.
Suppose the parcel changes hands at 14:02, the device scans it at 14:09, and the server records it at 14:15. Thirteen minutes pass between handover and record, and for that whole span the system does not know the parcel was delivered. A single “delivered” column mixes at least three facts: that the parcel physically changed hands, that the courier entered it on a device, and that the server processed the input and updated the state. The three timestamps do not always agree. Important business processes therefore separate the business state — how an entity such as a parcel or a contract ought to be treated — from the observed state, which is what a sensor, a person, or an external service reported, and from the processing state, which is how far the system has got with receiving, validating, applying, and notifying.
business state: delivered
observed state: signature captured by courier
processing state: delivery event applied, notification pendingWithout that separation there is no way to tell an email that merely failed to send from a transmission that is merely delayed from an entry the recipient disputes.
Defining Which Transitions Are Forbidden
Handling state properly means defining transitions, not just state names. The basic elements are the current state, the event that occurred, the condition permitting the transition, and the work performed during it. The W3C SCXML specification likewise defines states, transitions, and events as the basic concepts of a state machine, with a model that selects the matching transition on an event and runs the associated work.
accepted
├─ stock reserved → reserved
├─ stock unavailable → on hold
└─ cancellation request → cancelled
reserved
├─ payment succeeded → confirmed
├─ payment failed → awaiting payment
└─ cancellation request → cancellingIt is not only the permitted transitions that need defining. Whether shipped can go back to accepted, whether a cancelled order can be resumed, what happens when the same payment-success event arrives twice, and what happens when a late success notification follows a payment failure all need answers. Defining state transitions is the work of settling what the business means, before any of it is turned into code.
States a Boolean Cannot Express
The range of transitions defined in the previous section disappears when state is expressed as booleans. In a shape like is_active, is_paid, is_delivered, or is_approved, which only hold true and false, states such as unknown, being checked, partially complete, or stale get forced into one side or the other. Health monitoring for a device may need to distinguish a normal response, a slow response, a transient communication failure, a run of consecutive failures, monitoring being stopped, an observation that has gone stale, and scheduled maintenance.
Having distinguishable states is not enough; without deciding where the judgement boundaries lie, the state will not settle. When an observed value oscillates around a threshold, the state flips repeatedly between healthy and failed, so use different thresholds for declaring a fault and for declaring recovery, require a number of consecutive observations, and set a minimum duration and a freshness limit on the data. These are known as hysteresis, debouncing, and timeouts. Digitising is not the work of converting values into integers and strings but of making the uncertainty of observation and the boundary conditions of judgement explicit.
Recording Change, and Keeping It Correctable
Overwriting only the current state loses the reason it became that state. Event Sourcing stores changes to application state as a sequence of events, which makes reconstructing past states and recomputing after a correction possible. Not every system needs Event Sourcing, but for important state changes a record is worth keeping. What to keep: what happened to which entity, and who or what reported it. The two timestamps — when it happened in reality and when the system learned of it — and the rule used to decide are needed too. The previous state, the new state, the related operation ID, and the event being corrected are added to that.
Two Time Axes: Valid and Recorded
Where a business allows later correction, the time something was valid in reality has to be separated from the time the system recorded it. Bitemporal History handles both axes: when a fact was true in the world, and when the system knew it. A contract change that took effect on 1 August but was registered on 5 August looks like this.
effective_at = 2026-08-01
recorded_at = 2026-08-05A single updated_at cannot distinguish the contract that was actually in force on 3 August from the contract the system believed was in force on 3 August. Information lost to splitting, rounding, classification, or aggregation cannot be reproduced afterwards, so where correction, audit, or rule changes are possible, keep the original observations and events.
5. Splitting and Recombining — Choosing What the Seams Cost
Splitting does not remove complexity; it moves it from the inside to the seams. Design therefore has to settle where to cut and how to put the pieces back together at the same time. The measure of a split is likewise not how small the parts are but how manageable the seams turn out to be.
Where Design Starts — The Final Business Decision
Real design usually starts by defining the final business decision. Not “register an order” but “decide whether this order can be accepted and return the result”, with a deadline for when the result is needed. A screen response, machine control, a nightly batch, and a monthly close allow very different amounts of time. Next, extracting the invariants that must never be broken determines what belongs in the same transaction. The identifiers that decide what counts as the same entity get settled here too, keeping order ID, contract ID, customer ID, device ID, process ID, and event ID distinct.
From there, place business boundaries where vocabulary, responsibility, rules, and invariants change, and check that the main use cases complete inside one partition. If frequent cross-partition queries or distributed updates turn out to be necessary, revisit the partition key. Build the dependency graph, separate what can run in parallel from what must run in order and what can wait until after the response, and decide where the unit of failure sits — an item, a chunk, a stage, a transaction, or a whole saga. Distinguish occurrence time, receipt time, processing time, and settlement time, and define states such as unknown, on hold, expired, cancelled, and corrected.
How Each Kind of Split Gets Recombined
Split information and you need joins; split business processes and you need handovers of responsibility. Split processing and you need intermediate states and retries; split time and you get late data and cut-off times. Split state and several views have to be reconciled. Split design means deciding how to divide and how to recombine at the same time.
What a seam requires is determined by what was split.
| What was split | What recombining requires |
|---|---|
| Columns | IDs, schema, the meaning of the relationship |
| Records | Keys, ordering, deduplication |
| Partitions | Routing, aggregation, redistribution |
| Business domains | APIs, events, vocabulary translation |
| Processing stages | Messages, checkpoints |
| Transactions | Compensation, reconciliation, convergence conditions |
| Time windows | Watermarks, tolerance for lateness |
| State | Transition rules, event history |
A good split crosses boundaries rarely and has a clear contract when it does. A bad one crosses several boundaries per operation, has parties writing to shared data, and requires each side to know the other’s internals.
Consistency Models as Convergence Conditions
For asynchronous and distributed processing, saying it will be eventually consistent is not a convergence condition. Only once you have decided by when it converges, how a failure to converge gets detected, which side wins, who performs the correction, and how it is presented to users does it work as a consistency model.
Judging a Split
A split cannot be judged by how small the parts are. It is judged by whether change stays local, whether one decision completes inside one boundary, and whether the invariants that must hold can be kept local. Whether load distributes evenly enough to finish within the deadline, and whether failures stay contained and can be safely re-run, belong to the same set of criteria. Whether the split results can be correctly recombined, and whether drift from reality can be detected and corrected, complete the picture. Splitting does not reduce the total amount of complexity. Choosing which complexity to seal inside a boundary and which to coordinate across one, and making the cost of the seams explicit, is what brings a system within a workable scale, schedule, and set of responsibilities.
References
- ACM — On the Criteria To Be Used in Decomposing Systems into Modules (Parnas, 1972)
- Google Research — Dremel: Interactive Analysis of Web-Scale Datasets
- PostgreSQL — Table Partitioning
- PostgreSQL — LIMIT and OFFSET
- Apache Flink — Timely Stream Processing
- Apache Kafka — Documentation
- Martin Fowler — Bounded Context
- microservices.io — Decompose by subdomain
- Alistair Cockburn — Hexagonal Architecture
- ACM Queue — Life Beyond Distributed Transactions (Pat Helland)
- microservices.io — Saga
- ACM — Validity of the single processor approach (Amdahl, 1967)
- Google Research — The Tail at Scale
- Reactive Streams
- Google SRE Book — Addressing Cascading Failures
- gRPC — Deadlines
- AWS Well-Architected — REL04-BP04 Make mutating operations idempotent
- W3C — State Chart XML (SCXML)
- Martin Fowler — Event Sourcing
- Martin Fowler — Bitemporal History