The codebase modularity decision record: why the modularity boundary model you chose determines your build time ceiling and your cross-team coupling accumulation rate
Codebase modularity decisions are not architecture diagram preferences or platform strategy debates. They are a set of founding decisions about what constitutes a module boundary as distinguished from a technical layer boundary, how module boundaries are selected to align with the team's actual change patterns rather than with an idealized separation of concerns, how cross-module dependencies are governed before coupling accumulates silently in integration test suites and interface change request queues, and what mechanism triggers a boundary review before CI times exceed twenty minutes and cross-team coordination overhead begins consuming more engineering capacity than the features the teams are building — decisions with structural consequences invisible in sprint velocity data or code review metrics until build time ceilings or cross-team coupling accumulation rates reveal that the modularity model is setting a coordination tax on every feature the team ships.
A 25-person developer tools company building a CI analytics platform made its core architecture decision eight months into building the product. A newly hired principal engineer, arriving from a company that had successfully migrated from a monolith to microservices over two years, proposed applying the microservice model from the start: six services, one per domain, each with its own database, independently deployable, communicating over a message bus. The team — four engineers at the time — spent three weeks designing the service boundaries. The services were: an auth service handling identity and permissions, a user service managing account metadata and preferences, a billing service owning subscription and payment state, an extraction service running the core AI analysis pipeline, a notification service handling email and webhook dispatch, and a reporting service owning the dashboard query layer. The boundaries felt clean on the whiteboard. The team deployed the first version with all six services running.
By month eight of the microservices architecture, the team had grown to 11 engineers and had shipped 34 features. The lead engineer, preparing a capacity planning document, analyzed the prior 34 feature PRs to understand where engineering time had gone. The analysis revealed that 24 of the 34 features — 70% — had required coordinated changes across three or more services. Features that added a new analysis type to the extraction service required changes to the extraction service, the reporting service, and the user service (for per-user configuration), with three simultaneous deployments ordered carefully to avoid schema migration collisions. A feature that changed how billing state was communicated to the notification service required a message bus schema migration, a backward-compatible notification service deployment, and a billing service deployment — all coordinated within a 40-minute deployment window to avoid a state inconsistency window during the rollout. The CI pipeline, which ran integration tests across all six services simultaneously, had grown to a 45-minute build time as each service's integration test suite had expanded to cover the cross-service scenarios that were the most likely source of production failures. Engineers had begun skipping the integration test suite for changes they assessed as "obviously safe," which had produced three production incidents in the prior month where an "obviously safe" change in one service had produced an unexpected state in another.
The team spent four months consolidating the six services into a modular monolith with four well-bounded domain modules: identity (auth + user), billing, analysis (extraction + reporting), and notification. The module boundaries were chosen by analyzing which service pairs had been changed together most frequently in the prior 34 features — auth and user had been changed together in 18 of 24 features requiring multi-service changes; extraction and reporting had been changed together in 17 of 24. The consolidation produced a full test suite build time of 12 minutes, cut the median feature release time from 3.5 days (coordinating multi-service deployments) to same-day, and eliminated the integration test skipping problem because the most complex integration tests were now unit tests within the same process boundary. The principal engineer who had proposed the microservices architecture, reflecting on the decision in a post-mortem, identified the root cause: the service boundaries had been designed against the domain model's conceptual cleanliness rather than against the product's actual change patterns, and a domain model that is conceptually clean can be operationally coupled — the auth and user services were conceptually distinct but were operationally inseparable because every user-facing feature changed both. Connect this to the team topology decision record: the microservice boundary decisions the team makes at 4 engineers reflect the organizational structure of a company that will have 25 engineers in two years; a service boundary that feels clean with one team becomes a coordination boundary between two teams when the company grows, and a coordination boundary between two teams that ship features together most of the time is a coordination overhead, not an organizational investment — the service split that reduces operational coupling at scale is the split that the 25-person team's change pattern analysis would identify as a low-crossing-frequency boundary, not the split that the 4-person founding team identifies as a conceptually satisfying domain separation.
A 40-person B2B SaaS building a contract lifecycle management platform had adopted a modular monolith architecture early in its development. The engineering team had four engineers when the architecture was designed, and the module boundaries were drawn along the technical layer model that the lead engineer had used successfully at a prior company: a controllers module (HTTP request handling), a services module (business logic), a repositories module (database access), and a models module (domain objects). Each incoming feature request touched all four layers — a new contract type required a new controller endpoint, service methods for validation and state transitions, repository methods for persistence, and model classes for the domain representation. The layer architecture provided clear separation of technical concerns, and the team found that new engineers could orient themselves quickly: "put HTTP logic in controllers, business logic in services, database logic in repositories."
As the team grew from 4 to 14 engineers over 22 months, the layer architecture's structural problem became load-bearing. The services module — the layer with the most business logic — had grown to 182 files totaling 64,000 lines of code, owned by all 14 engineers without a formal ownership model. Every sprint, multiple engineers were making changes to the services module simultaneously. Merge conflicts in the services module consumed between 2 and 4 hours per sprint across the team, measured by the engineering director through a 3-month calendar review. The conflicts were not caused by engineers working on the same feature — they were caused by engineers working on unrelated features who had independently added service methods to the same large service class, producing structural conflicts in a file that had grown large because the architecture gave it no reason to split. The test suite had grown to 3,800 tests running in 28 minutes, with 65% of the test count in the services layer — because the services module was where all the business logic lived, all the test complexity also lived there, and the test suite for the services module had grown into a slow integration test suite that tested the services module's interaction with both the repositories layer and the models layer rather than testing business logic in isolation.
A domain-driven refactor, completed over 4 months with a team of 14, restructured the codebase into 6 domain modules: contracts, parties, obligations, events, search, and billing. Each domain module owned its own controller, service, repository, and model classes — the technical layer separation was preserved within each domain, but the module boundary was drawn at the domain boundary rather than at the technical layer boundary. The refactor reduced cross-module merge conflict frequency by 80% (measured in the first quarter post-refactor), because engineers working on the contracts domain and engineers working on the billing domain no longer touched the same service files even when their feature work was structurally similar. The test suite build time dropped from 28 to 14 minutes, not because tests were deleted but because the domain module structure allowed the test suite to run domain-by-domain, and a change to the contracts module's service layer no longer required running the billing module's integration tests — the domain boundary provided the test isolation that the technical layer boundary had not. Connect this to the code review policy decision record: the merge conflict rate in a shared module is a proxy for the module's ownership model — a module with no named owner and no change boundary is a module that every engineer treats as unowned, producing changes that are locally correct and structurally conflicting; the code review process in a high-conflict module typically surfaces the symptoms of the ownership problem (conflicts, regressions in unrelated areas) without identifying the cause (the module boundary is incorrect), and raising the bar for code review quality in a module with a misaligned boundary produces slower reviews of the same conflict rate rather than addressing the structural source of the conflicts.
A 60-person enterprise SaaS building a financial reporting platform had extracted its core calculation engine as a separately governed module three years before this session. The extraction had been well-motivated: the calculation engine was responsible for producing the regulatory-compliant output numbers that the company's customers submitted to auditors, and any defect in the calculation engine had a customer trust impact disproportionate to its engineering complexity. Making the calculation engine a separately governed module with a strict public API — 12 named methods, each with a versioned signature, each with its own test suite producing a test report that auditors could review — had been a sound decision at the time it was made. The module had two dedicated engineers, a formal review protocol for any change to the public API, and an interface change request (ICR) process by which product engineering teams could propose additions to the public API. The calculation engine had shipped 47 new use cases in three years, all correctly, with zero regulatory audit findings.
By the third year, the ICR process had become the calculation engine team's largest capacity consumer. There were 23 open ICRs, averaging 6 weeks from submission to resolution. The calculation engine team was spending 20% of its capacity reviewing incoming ICRs, conducting design sessions with requesting teams, and managing the backward compatibility implications of accepted changes. The product engineering team was spending 15% of its capacity on ICR-related work: writing ICR proposals, attending design reviews, building workarounds for approved-but-not-yet-implemented ICRs, and managing the 6-week wait between a feature's design and the interface capability required to implement it. The calculation engine's public API had grown from 12 to 31 methods in three years as ICRs were approved, but the method set had accreted without a coherent design review of the API as a whole — each individual ICR had been evaluated for correctness in isolation, and the resulting 31-method API had multiple overlapping methods that calculated similar quantities through slightly different paths, producing a surface area that was harder for new engineers on both teams to use correctly than the original 12-method API had been.
The ICR backlog was diagnosed not as a governance failure — the ICR process was doing what it was designed to do — but as a modularity architecture failure: the module's public API was designed as a stable, auditor-visible interface, but 70% of the open ICRs were for internal use cases that the product engineering team needed to prototype quickly, where a 12-month backwards compatibility guarantee was more constraint than the use case required. The team introduced a tiered interface model: a stable API (the existing 12 auditor-visible methods, unchanged, 12-month backwards compatibility guarantee), an experimental API (new methods with a 90-day deprecation guarantee and no audit reporting requirement), and a plugin extension point that allowed product engineering teams to register calculation plugins that the engine would invoke at defined extension points without the teams filing ICRs at all. The experimental API reduced the ICR queue from 23 to 4 structural stable-API requests within one quarter, because 19 of the 23 open ICRs were for new use cases where the product team needed to prototype the calculation approach before the abstraction was clear enough to commit to the stable API. The plugin extension point eliminated the class of ICR where a product team needed calculation behavior that was specific to one customer's reporting format and that the calculation engine team correctly judged should not be part of a shared interface. Connect this to the vendor dependency governance decision record: the interface governance problem in a strict module API is structurally similar to the vendor dependency problem in an external API — the requesting team needs capability faster than the governance process provides it, and without a legitimate fast-track path, the requesting team builds workarounds that accumulate the same coupling outside the governed boundary that the boundary was designed to prevent inside it; a module governance model that provides no experimental or provisional interface track will produce an informal workaround track, and the workaround track's coupling is harder to govern than the experimental API's coupling because it is invisible to the module owner.
Structural properties set by the codebase modularity decision
Two structural properties are determined by the founding decisions about modularity model, module boundary criteria, dependency governance, and evolution cadence. Neither is visible in the sprint velocity data or the code quality metrics that engineering teams typically track until CI build times exceed the threshold where engineers begin skipping tests, or cross-team coupling accumulation rates produce sprint planning discussions that are primarily about dependency negotiation rather than feature scope.
Property 1: Build time ceiling. The modularity model sets a floor below which the full test suite build time cannot be reduced regardless of hardware investment or test parallelization. The floor is determined by the number of integration boundaries the test suite must cross in order to verify the system's correct behavior — a module boundary that is correctly aligned with the codebase's change patterns requires only unit tests within each module and interface contract tests between modules; a module boundary that is incorrectly aligned with the change patterns requires integration tests across the boundary for every feature that crosses it, because the boundary creates a coordination seam that the unit tests on each side cannot independently verify. The build time ceiling accumulates invisibly: the first cross-boundary integration test added to the suite takes 2 seconds; the hundredth takes 180 seconds; the test suite at month 18 is running in 45 minutes not because 45 minutes of tests were added, but because 45 minutes of tests were added to cover the integration seams created by a modularity model that is crossing the team's highest-frequency change boundaries. The ceiling reveals itself through a second-order effect: engineers stop running the full test suite before pushing changes. A team that selectively runs tests to avoid a 45-minute CI build is not making a deliberate speed-quality tradeoff; it is making a coverage tradeoff imposed by the modularity model's boundary misalignment, and the coverage gap compounds in the parts of the codebase that are slowest to test and therefore most often skipped — which are also the cross-module integration surfaces most likely to be the site of production incidents. A modularity model where each module's test suite can run in isolation, with a separate integration test suite that runs at deploy time rather than at commit time, is a modularity model that has correctly aligned its boundaries with the team's change patterns: the unit tests on each module pass quickly because the module's internal logic is tested without cross-boundary setup, and the integration test suite is small because the cross-boundary interactions are few and well-defined. Connect this to the technical debt prioritization decision record: a build time ceiling imposed by boundary misalignment is a technical debt that accumulates in engineering capacity rather than in code quality — the cost is paid every sprint in the form of slower commit cycles, skipped test runs, and the additional debugging time when skipped tests would have caught the regression; the decision to invest in a modularity refactor is a technical debt payoff with a measurable return in engineer-hours recovered per sprint, not an abstract architecture improvement whose value is argued from principle rather than from measurement.
Property 2: Cross-team coupling accumulation rate. The rate at which new features require coordination between teams is determined by how well the module boundaries were chosen relative to the team's delivery patterns — not relative to the architecture's idealized separation of concerns. A module boundary that frequently requires crossing for routine feature work creates a compounding coupling debt with two components. The first is soft coupling: dependencies between modules that are added informally, one feature at a time, when the interface boundary does not support the feature's requirements and the engineer adds a dependency directly rather than filing an interface change request. Soft coupling is invisible to the module owner, accumulates in the codebase's import graph rather than in the interface governance record, and produces regressions in one module when the other module's internal structure changes in a way that the soft dependency had not anticipated. The second is governance overhead: when the interface governance process is used correctly, each feature that requires a cross-boundary capability generates an ICR or a design session, which consumes coordination capacity from both the requesting team and the module owner's team. A feature set that requires 20 ICRs per quarter across a team of 15 engineers is spending roughly one engineer-week per quarter on interface governance overhead — not engineering the features, but negotiating the interfaces between the modules responsible for delivering them. The coupling accumulation rate is visible in three leading indicators before it reaches a capacity crisis: the fraction of sprint planning tasks that include a cross-team dependency as a precondition (a ratio above 30% is a boundary misalignment signal), the median time from feature design to first commit in modules that frequently require ICRs (a gap above 3 weeks indicates that the interface governance is on the critical path of feature delivery rather than off it), and the ratio of module interface change requests to module internal changes in the prior quarter (a ratio above 1:3 indicates that the module's boundary is so tight that it cannot evolve with the product without constant interface renegotiation). Connect this to the engineering team communication channel decision record: cross-team coupling accumulation produces a specific communication overhead pattern — the cross-module dependency negotiation conversation that recurs in sprint planning, in architecture review, and in the code review when a module boundary crossing is discovered in a PR — and the communication channel model determines whether these conversations are synchronous (consumed from meeting time, creating serialized decision latency) or asynchronous (consumed from review queue time, creating parallel decision latency but higher context-switching overhead); a module boundary that requires frequent cross-team negotiation will produce high overhead regardless of the communication channel model, and the channel model's job is to minimize the overhead of necessary negotiation rather than to substitute for the boundary redesign that eliminates it.
The codebase modularity ADR: five sections
Section 1: Modularity model selection and rationale. The modularity model specifies the structural approach to code organization: a monolith deploys all code as a single unit with no enforced internal boundaries; a modular monolith deploys as a single unit but enforces internal module boundaries through build tool rules or package visibility constraints; a microservices architecture deploys each module as an independently runnable process with network communication between modules. Each model has a distinct cost-benefit profile relative to team size and change pattern density. The monolith's primary benefit is zero cross-module coordination overhead — there are no module boundaries to cross, no interface governance processes to maintain, and no deployment coordination requirements; its primary cost is that all engineers share a single process boundary, which means that a change to any part of the codebase can affect any other part without a build tool enforcing isolation, and the discipline required to maintain logical module separation without tool enforcement degrades under sprint pressure and team growth. The modular monolith's primary benefit is enforced isolation at a lower operational cost than microservices — the module boundaries are encoded in build rules that prevent unauthorized dependencies, but the deployment unit remains a single process that requires no service mesh, no distributed tracing infrastructure, and no distributed transaction coordination; its primary cost is that the boundary enforcement tool must be actively maintained, and a modular monolith where the enforcement tool is misconfigured or ignored is a modular monolith in name only. The microservices model's primary benefit is deployment independence — each service can be deployed, scaled, and updated independently without coordinating with other services — and its primary cost is that every module boundary becomes a network call, a distributed system coordination problem, and an interface governance surface; a microservices architecture where the services are tightly coupled through high-frequency synchronous calls has paid the operational cost of microservices without receiving the deployment independence benefit, which is the premature-split failure mode. The selection decision should be made against the team's current and projected change pattern density, not against an idealized future architecture: a team shipping 5 features per week with a change pattern analysis showing 70% cross-module changes is a team that is better served by a modular monolith with 3 well-chosen boundaries than by a microservices architecture with 6 cleanly-separated services, because the change pattern density means the services will be tightly coupled regardless of the boundary design, and the modular monolith's enforcement mechanism makes that coupling visible rather than operationally expensive.
Section 2: Module boundary criteria and change pattern alignment. Module boundaries are correctly placed when they minimize cross-boundary change frequency while maximizing internal cohesion — the module boundary is a seam across which changes least often need to cross together, not a seam across which the domain model looks most cleanly divided. The change pattern alignment analysis is the primary method for validating proposed boundaries before they are encoded in build rules or service deployments: for each proposed boundary, count the number of commits in the prior 6 to 12 months where changes on both sides of the boundary appeared in the same pull request. A proposed boundary that would have been crossed in 50% or more of feature PRs in the prior 12 months is a boundary that provides isolation at the cost of a coordination overhead on every feature — the boundary is in the wrong place. A proposed boundary that would have been crossed in fewer than 15% of feature PRs in the prior 12 months is a boundary that provides isolation aligned with the actual change patterns — the boundary correctly separates code that changes independently most of the time. For new codebases where 12 months of change history does not exist, the proxy method is domain event analysis: a domain event (a fact about the system that changes when a domain action occurs — "contract was approved," "user was billed," "notification was sent") should propagate to one module. If a single domain event requires state changes in multiple modules simultaneously, the module boundaries are drawn across a domain event seam rather than around it, which is the signature of the wrong boundary placement. The corrective is to redraw the boundary so that each domain event's state change is owned by a single module, and the other modules learn about the event through an event notification rather than through a direct state update. Module boundary criteria must also specify the enforcement mechanism: a modular monolith boundary enforced by a dependency analysis tool (module A's code is forbidden from importing module B's internal packages; violations are a build failure) degrades less under growth than a boundary enforced by documentation and code review, because the enforcement is automated and immediate rather than manual and periodic. The boundary enforcement tool should be configured at the time the boundary is drawn, not added later — adding enforcement to existing code produces a large backlog of existing violations that must be resolved before the enforcement is useful, and the resolution work often reveals that the boundary was drawn in the wrong place for the existing code to comply with it.
Section 3: Inter-module dependency governance model. The dependency governance model specifies how cross-module dependencies are approved, documented, and reviewed — the rules that determine when a module's internal code may be used by another module, who approves exceptions, and how the module's public interface evolves over time. The governance model has two components: the interface design model and the change request process. The interface design model specifies what constitutes the module's public contract: the types, methods, events, and data formats that other modules may depend on, and the stability guarantee attached to each. A stability tier model divides the public contract into stable (12-month backwards-compatibility commitment, changes require a formal review), experimental (90-day backwards-compatibility commitment, changes require notification but not formal review), and deprecated (removal-scheduled, changes and removals are broadcast in advance with a migration path). The stability tier model allows the module owner to iterate on new interface capabilities without committing to a backwards-compatibility guarantee that would prevent the interface from being revised once the right abstraction becomes clear — the experimental tier is the mechanism for shipping new interface capability fast while reserving the right to improve the abstraction before it becomes a stable commitment. The change request process specifies how requesting teams propose additions to the module's interface. A good change request process has a fast path for experimental additions (48-hour review, since the commitment is limited) and a standard path for stable changes (2-week review including a design session, since stable changes are difficult to reverse). The process should produce a searchable record of why each interface decision was made — not just what was added, but what alternatives were considered, why the chosen abstraction was preferred, and what use cases were explicitly out of scope. Design sessions in ChatGPT or Claude where the interface addition was explored — what should the method signature be, should it be a query or a command, what happens to existing callers if the input type changes — should be run through the open-source extractor to recover the explicit reasoning before the interface governance record is written, because the design session contains the trade-offs considered and rejected, and a governance record written without that context will be read by the team member 18 months later who is trying to understand why the abstraction was chosen — the record that says only what was decided, not why, produces the same question-with-no-answer problem that the governance process was designed to prevent.
Section 4: Build time and coupling metrics. The build time and coupling metrics model specifies what is measured, how frequently, and what thresholds trigger a modularity review. Build time measurement: the full test suite build time is recorded for every commit to the main branch, tracked as a rolling 30-day median, and reviewed monthly. The thresholds are: under 10 minutes (healthy), 10 to 20 minutes (watch), 20 to 30 minutes (review trigger), above 30 minutes (intervention required). The review trigger is not the build time itself but the rate of growth: a build time growing by more than 15% per month indicates that the test suite is accumulating integration tests faster than the modularity model can absorb them, which is the early signal of a boundary misalignment problem that is much cheaper to address at the watch threshold than at the intervention-required threshold. Coupling measurement: the cross-module change frequency is recorded by analyzing the prior month's merged pull requests and counting the fraction that modified files in more than one module. The thresholds are: under 20% cross-module change frequency (healthy), 20% to 35% (watch), above 35% (boundary review trigger). The coupling measurement is separated by module pair: a 25% overall cross-module change frequency may be acceptable if it is distributed evenly across many module pairs, and unacceptable if it is concentrated in one module pair that is crossing in 60% of all feature PRs — the concentrated case indicates a module boundary that is incorrectly placed between two modules that belong together. Interface change request metrics: the open ICR count, the median ICR age, and the fraction of sprint planning tasks with a cross-module ICR dependency are tracked monthly. The open ICR count above 10 or the median ICR age above 4 weeks triggers a governance model review — not an individual ICR review, but a review of whether the governance process is appropriately calibrated for the module's current interface evolution rate.
Section 5: Module evolution and boundary review cadence. The module evolution model specifies the conditions under which a module boundary may be moved, split, merged, or eliminated, and the review cadence that identifies whether the current boundary set remains aligned with the team's change patterns as the product evolves. Module boundary changes are not routine maintenance — they are structural decisions with codebase-wide impact — and the review cadence ensures they are triggered by evidence rather than by preference. The boundary review cadence is semi-annual: every 6 months, the module boundary owner conducts a change pattern analysis for the prior 6 months and compares it against the analysis that was used to justify the boundary's current placement. A boundary that was placed on a seam crossed in 12% of feature PRs 18 months ago and is now crossed in 42% of feature PRs requires a boundary review: either the product has evolved to a state where the boundary is no longer correctly placed, or the boundary's enforcement has degraded such that the module that should have been separate is now being treated as a single unit. Off-cycle boundary review triggers: a build time crossing the 20-minute watch threshold; a cross-module change frequency crossing the 35% boundary review threshold; an open ICR count above 10; a sprint retrospective item citing cross-module coordination as a significant delivery friction more than twice in a quarter. Module split criteria: a module is a candidate for splitting into two modules when a change pattern analysis identifies two distinct cohesion groups within the module — two subsets of files that are almost always changed together internally but are almost never changed together across the two subsets. The split should be preceded by a 2-month period of enforcing a soft boundary within the module (a lint rule that flags imports across the prospective split line without failing the build), to confirm that the soft boundary is not crossed by active development before the hard enforcement is added. Module merge criteria: two modules are candidates for merging when the cross-module change frequency between them has exceeded 40% for two consecutive quarters, indicating that the boundary is providing a coordination overhead without providing a proportional isolation benefit. The merge decision should be preceded by a governance review of what the original boundary was designed to protect — a boundary that was drawn to isolate audit-critical calculation logic from product feature logic should not be merged even if the change frequency is high, because the isolation is providing a governance property rather than an engineering property, and the governance property's value is not captured in the change frequency metric. Five ADR sections from the original modularity architecture sessions in ChatGPT or Claude — the session where the team debated microservices versus modular monolith, why the boundaries were placed where they were, what the change pattern data at the time showed, what the team's growth projection was, and what the trigger conditions would be for a split or consolidation — should be extracted and linked to this ADR record so that future boundary reviews can evaluate whether a proposed change is reversing a deliberate constraint or addressing a genuinely changed context. Running those design sessions through the open-source extractor before the semi-annual boundary review retrieves the explicit reasoning from the original architecture conversation, including the alternatives considered and the failure modes the original boundary design was built to prevent, so the review team can distinguish between a boundary that should evolve and a boundary that was correctly placed and should be defended.
FAQ
How do you know when a modular monolith is ready to be split into separate services?
The signal that a modular monolith is ready to be split is not team size or line count — it is deployment independence: when two modules have demonstrably different deployment cadences and demonstrably different operational profiles, the cost of coordinating their deployments inside a monolith begins to exceed the cost of maintaining the interface boundary between two independently deployable services. The measurement approach is to track, for each pair of modules, how often a change in one requires a coordinated change in the other, how often their deployments overlap in the same release window, and whether their failure modes are operationally isolated. A module pair with low coupling, frequent deployment contention, and isolated failure modes is a good split candidate. A module pair with high coupling, rare deployment contention, and shared failure modes is a poor split candidate regardless of how clean the interface boundary looks in the architecture diagram. The most common premature split error is extracting a service because it feels conceptually separate, before the deployment independence signal is present — producing a distributed monolith where the modules are still tightly coupled through synchronous API calls on every feature but are now running as separate processes with full distributed system operational overhead.
What is the right way to define module boundaries in a growing codebase?
Module boundaries should be defined against change patterns, not against conceptual categories. The right boundary is the line across which changes least frequently need to cross together — a boundary rarely crossed in the same pull request is providing real isolation, while a boundary crossed in every feature PR is providing organizational overhead without isolation benefit. The practical approach is to run a change pattern analysis before formalizing module boundaries: for the prior 6 to 12 months of commits, count how many pull requests touched files on both sides of each proposed boundary. A proposed boundary that would have been crossed in 60% of feature PRs is not a useful modularity boundary. The best module boundaries emerge from the team's natural change cohesion: the files almost always changed together belong in the same module; the files almost never changed together belong in different modules. Domain-driven design provides useful vocabulary for this — bounded contexts, domain events — but the vocabulary is only useful as a labeling system for boundaries already identified through change pattern analysis, not as a design system applied top-down from the domain model.
How do you prevent module boundaries from eroding as the codebase grows?
Module boundary erosion is prevented by three mechanisms applied together. First, automated enforcement: the module boundary is encoded in a build tool rule that makes crossing the boundary a build failure rather than a code review judgment call. A boundary enforced by convention erodes whenever a developer under time pressure concludes that the exception is justified; a boundary enforced by a build rule erodes only when the team explicitly decides to change the rule. Second, owned boundaries: each module has a named team or named engineer as the boundary owner, responsible for approving changes to the module's public interface. A boundary without an owner is maintained by collective goodwill, which degrades under sprint pressure and team turnover. Third, regular boundary reviews: every 6 months, the boundary owner reviews the module's change log against the boundary's original rationale — is the boundary still providing the isolation it was designed to provide, or have the change patterns shifted such that the boundary now crosses a high-frequency change seam? A boundary correctly placed 18 months ago may be incorrectly placed today because the product evolved in a direction the original design did not anticipate. The review evaluates whether the product evolution requires a boundary evolution to maintain the isolation property the boundary was designed to deliver.
What does a good interface change request process look like for a module with a strict public API?
A good interface change request process is fast enough that teams use it rather than work around it. The structure: the requesting team submits a one-page proposal with four sections — the use case the new interface must support, why the existing interface cannot support it, a proposed interface addition, and an assessment of whether this is a stable-API change (12-month backwards-compatibility commitment) or an experimental-API addition (90-day deprecation window). The module owner's response has two tracks: a fast-track for experimental additions (48-hour review) and a standard-track for stable changes (2-week review including a design session). The most important structural element is the experimental-API track: most interface change requests are for new use cases where the right abstraction is not yet known, and committing them to the stable API immediately produces an API that accumulates poorly-abstracted methods. An experimental API gives the module owner a mechanism to say yes quickly to new use cases while reserving the right to revise the abstraction before it becomes a stable commitment that requires coordinating deprecation across all callers.
Further reading
- Team topology decision record — the team topology model and the modularity model are structural complements: module boundaries that were correctly placed for one team topology become coordination seams when the team topology changes; a split from a single platform team into a platform team and two product stream teams will reveal which module boundaries were drawn against the prior team structure and which were drawn against the product's change patterns — the boundaries that survive the team topology change without becoming high-frequency crossing boundaries are the boundaries aligned with change patterns; the ones that become immediate cross-team coordination overhead are the team-structure boundaries that should be redrawn against the new topology's delivery pattern before the coordination overhead accumulates into a sprint planning recurring agenda item.
- Code review policy decision record — the code review process is the primary detection surface for soft coupling between modules, because soft coupling appears as an import that crosses a module boundary without triggering the build enforcement rule (a pattern that happens when the enforcement rule is incomplete) or as a design choice that mirrors a module's internal structure in a way that produces tight coupling without violating the interface contract; a code review process with a module boundary check as a mandatory review dimension — does this change introduce a dependency on a module's internal structure rather than its public interface — catches soft coupling at the PR stage rather than in the post-merge period when it has been deployed and is influencing subsequent design decisions; the reviewer's knowledge of the module boundary's original rationale is the judgment asset that makes the check effective, which is why the design session that produced the boundary criteria must be in a searchable record rather than in the reviewer's recall.
- Technical debt prioritization decision record — build time ceiling debt is one of the most predictable technical debts to quantify: the per-sprint cost is the number of engineer-hours consumed by slow CI runs, by selective test execution that increases defect rate, and by the debugging overhead when skipped tests would have caught the regression; at a 45-minute CI build with 11 engineers each running 3 test cycles per day, the CI time alone is 24.75 engineer-hours per day — more than three full engineer-days consumed by waiting for feedback that a correct boundary placement would reduce to 6 hours; the payoff calculation for a modularity refactor is therefore measurable before the refactor begins, not estimated after, and the prioritization decision is whether the refactor payoff in recovered CI time exceeds the refactor's 3-to-4-month opportunity cost against the features that would otherwise ship during that window.
- Vendor dependency governance decision record — the inter-module interface governance problem and the vendor dependency governance problem share the same structural failure mode: the requesting party needs capability faster than the governance process delivers it, and without a legitimate fast-track path they build workarounds that accumulate informal dependencies outside the governed surface; a module governance model without an experimental-API track will produce workarounds for the same reason a vendor governance model without a provisional approval path will produce shadow IT — the governance process's job is to provide a legitimately fast path for the low-commitment case so that the slow path is reserved for the high-commitment case that genuinely requires it, and a governance model with only a slow path produces a bypass rate proportional to the sprint pressure the requesting teams are under.
- Open-source extractor — modularity architecture sessions in ChatGPT or Claude contain more explicit reasoning about boundary trade-offs than most architecture decision records: the session where the team debated whether to extract the calculation engine as a separate service or a governed module contains the failure mode analysis the team ran on both options, the deployment independence signal they used to make the boundary decision, the metric thresholds they set for triggering a boundary review, and the interface stability model they chose and why — running those sessions through the extractor before the semi-annual boundary review retrieves the original reasoning from the design conversation rather than from the boundary owner's recall two years later, which means the review team can evaluate whether a proposed boundary change addresses a genuine changed context or reverses a deliberate constraint that was built in response to a specific failure pattern the original team had observed; the boundary that looks arbitrary two years later was usually motivated by a failure that was vivid at the time and is invisible in the boundary's current state.