Originally published on Medium on April 17, 2025. Edited October 26, 2026. This revision develops the original argument with practical engineering experience, technical corrections, and new illustrations. The original Medium illustrations remain available at the source.
During work on a mobile authentication path at Manulife, several approaches to asynchronous work had accumulated: RxJS, Redux thunks, and Promises. Each served a purpose, but their coexistence made an important question harder to answer: which operation owned a side effect, and which session did a late response belong to?
A perfectly organized directory tree would not have answered it. The solution required clearer responsibility for state, effects, and the contracts between them.
That experience captures what I value in software architecture. Architecture is not measured by the number of layers or patterns. It is measured by how much a system can change without disrupting behavior that should remain stable. Sometimes a module boundary helps. Sometimes the important boundary is a runtime invariant, an API contract, an approval decision, or a migration that the team can actually complete.
This article considers how to choose those boundaries, how Clean Architecture and SOLID can help, and what experience with existing products, security, and AI-assisted engineering adds to the textbook diagrams.
Contents
- The Economics of Independence
- Boundaries and Contracts
- Modernizing Systems That Already Exist
- Architecture Across Products and Platforms
- AI: Provider, Authority, and Behavioral Independence
- Security, Testing, and Enforceable Conventions
- Choosing Boundaries Worth Maintaining
- Conclusion
- References
1. The Economics of Independence
The familiar project management triangle describes tensions among scope, schedule, and cost. Architecture affects these pressures by determining how widely a change propagates, how much coordination it requires, and how confidently its effects can be verified.
Consider a login-provider replacement. If credential handling, presentation state, provider SDK calls, analytics, and persistence are interwoven, changing one provider may touch all of them. If the use case owns a meaningful authentication contract, some of that change can stay in one adapter. The application still needs integration tests and security review, but the migration has a boundary.
I saw a related benefit at QuickMobile/Cvent, where a configurable event framework supported different conference experiences. The reusable platform and event-specific configuration needed separate responsibilities. A fix in shared behavior could then help many events rather than becoming another client-specific change. This was product architecture as much as code organization: an understanding of which capabilities were common and which varied.
Independence does have a price. A new interface, adapter, package, or service introduces concepts that must be learned and maintained. Extracting six layers for a small operation may increase costs without containing meaningful variation. The relevant question is not whether the resulting diagram looks clean. It is which foreseeable change, responsibility, or risk the new boundary isolates, and whether that benefit justifies its ongoing cost.
2. Boundaries and Contracts
Clean Architecture organizes systems around enduring application policy rather than specific UI, storage, or network frameworks. Its dependency rule concerns source code: important policy should not have to import replaceable infrastructure to express its behavior.
A useful example is authentication. Presentation code can call an input port exposed by an application use case. The use case may depend on an output port describing the session store or identity-provider capability it needs. A concrete HTTP or storage adapter implements that output contract. The UI-facing input port and infrastructure-facing output port have different responsibilities; they are not one interchangeable interface.
At runtime, execution can cross these boundaries in both directions. An application use case may call an external service through its injected adapter, even though the adapter’s source code depends inward on the application-owned contract. Runtime call direction and source dependency direction must not be confused.
2.1 SOLID as a set of review questions
SOLID principles help evaluate the contracts inside this structure. They do not prescribe one framework or require an interface for every class.
Single Responsibility (SRP) asks whether a module has one principal reason to change. Login policy, display formatting, and analytics reporting may happen in the same user journey without belonging to the same responsibility.
Open-Closed (OCP) favors stable contracts that permit expected variation. Supporting a new authentication provider behind a suitable port can avoid repeatedly altering login policy. This is not a prohibition on modifying code.
Liskov Substitution (LSP) requires implementations to preserve the behavior expected by callers, not just method signatures. A token validator that unexpectedly accepts expired credentials is not substitutable for one whose contract rejects them.
Interface Segregation (ISP) keeps clients dependent on the operations they need. Credential verification should not force a use case to depend on identity-provider administrative operations.
Dependency Inversion (DIP) keeps policy from depending directly on low-level implementation details. Dependency injection is one way to assemble implementations; it is not itself the principle.
These concepts are most effective when attached to a concrete contract: expected inputs, outputs, invariants, errors, side effects, and ownership. An interface with the right method names but unspecified retry, expiry, or cancellation behavior may still leak the very detail it was meant to isolate.
2.2 Packaging expresses ownership
There are several practical ways to package the same dependencies. A layer-first structure groups presentation, application, domain, and infrastructure code. A feature-first structure keeps an authentication or synchronization capability together, perhaps with internal layers. Ports and adapters can be used within either structure; reusable components may also expose narrower public contracts.
I favor an arrangement that reveals what changes together and who owns it. A feature module that reaches into another feature’s internals has not gained much independence. Nor has a repository interface that sends database-specific rows across the entire application. The contract, rather than the folder or pattern name, is the architectural asset.
3. Modernizing Systems That Already Exist
Architecture becomes harder when a product already has users, undocumented behavior, platform constraints, and commitments that cannot be paused. This is where architectural principles need to work with incremental delivery instead of demanding a clean-sheet rewrite.
3.1 Authentication: structure is not enough
The problem was not simply which asynchronous mechanism to use. It was whether the state that justified an operation was still current when the operation finished.
The useful response was to clarify the rules for reading current state, coordinating effects, and accepting or rejecting results. A late response associated with an earlier authentication request should not blindly overwrite state for a newer one. Tests can exercise these invariants without requiring every implementation to be replaced at once.
The work improved the direction for evolving the authentication path. It did not eliminate every asynchronous framework or complete a wholesale Clean Architecture rewrite. That distinction matters: a boundary is useful because it makes the next safe change possible, not because it certifies that the entire system has been transformed.
3.2 A prototype is not a migration
A separate Manulife analytics modernization exposed coupling among authenticated context, asynchronous events, and analytics mappings. The concern extended beyond inaccurate metrics: stale user context could enter telemetry, creating a privacy risk.
I developed a substantial Redux/thunk-based prototype with data-parity tests comparing generated analytics output against the existing implementation. This made it possible to investigate behavior and correctness before changing the production path. The prototype was not merged into the production application. Some colleagues adapted parts of the approach, but other delivery priorities took precedence.
The technical lesson is about migration evidence. The organizational lesson is equally important: passing parity tests does not create the schedule, ownership, or motivation required for adoption. An architecture that cannot be introduced in reviewable increments may be too expensive to realize, even if its intended end state is sound.
3.3 Change through seams, not aspiration
My default is to identify a risky behavior, document its observable contract, introduce a small seam, replace one implementation behind it, and compare results. Unit and contract tests help, but so do operational signals and a rollback plan. Transitional code should be removed when the new path is proven.
A complete rewrite can still be justified when safe incremental evolution is demonstrably more expensive. The decision should account for hidden integrations, data migration, feature parity, operational risk, security controls, and the cost of parallel systems—not just the appeal of an improved directory structure.
See Legacy Systems in the Real World for more on the delivery and organizational constraints surrounding modernization.
4. Architecture Across Products and Platforms
Different systems put pressure on different boundaries. At ePACT, mobile and backend work involved relational storage, local SQLite data, JSON client models, and incremental synchronization. The durable architectural question was the data contract between stores: how changes were identified, mapped, and applied. Requiring every tier to share a persistence representation would not have provided the same independence.
At OpenArchive, work involving SwiftUI and Jetpack Compose alongside Clean/SOLID design aimed to separate platform rendering from privacy-sensitive behavior. A view model or presenter can transform application results into testable display state without moving domain policy into the view. The Humble Object pattern expresses a related idea: keep framework-heavy, difficult-to-test mechanics small while moving deterministic decisions into testable collaborators. That work should not be mistaken for a fully deployed three-tier automated test pyramid.
Mobile platforms still impose real constraints. Camera access, biometrics, credentials, background execution, offline behavior, and platform security APIs are not incidental capabilities. They can be mediated by adapters when useful, but some underlying semantics must remain visible in the application contract.
Legacy integration introduces another kind of pressure. A native client using authenticated WebViews cannot solve token expiry or backend ownership purely by reorganizing view classes. A backend-for-frontend (BFF) can provide a controlled API seam while older services evolve, although it creates another component to operate and secure. In the Manulife context, such integration boundaries were more realistic than waiting for every legacy dependency to disappear.
5. AI: Provider, Authority, and Behavioral Independence
AI systems add a particularly challenging external dependency: one that can satisfy an interface contract while producing inconsistent, incomplete, or unsafe results.
The usual ports-and-adapters argument still applies. An application may request inference through an interface while concrete adapters handle local or hosted models, provider credentials, prompt formats, rate limits, and response parsing. But swapping the API adapter does not establish three other properties automatically.
Provider independence means that a model or service can be replaced without rewriting application policy. It requires explicit handling of capabilities and limitations: context size, structured-output support, latency, cost, and data residency may differ.
Authority independence means that model output cannot confer permission on itself. A suggestion to modify a repository, delete a file, or run a command must still cross deterministic validation and an appropriate authorization boundary. The model can propose; the application decides whether execution is allowed.
Behavioral independence means that a replacement model meets the application’s actual acceptance criteria, not merely its transport schema. Two models may return syntactically valid JSON while differing in reliability, safety, error handling, or decisions under ambiguous input. That is a behavioral substitutability question related to LSP, with additional uncertainty requiring evaluation and monitoring.
In independent projects such as Anthesis and Dubnium, I have been exploring and implementing a separation between scoped context, proposals, deterministic checks, policy, constrained execution, and evidence. These are project-level engineering patterns, not a claim that one unified enterprise system has been deployed.
Context retrieval is part of the boundary. Prompts, retrieved documents, model identity, and history are inputs with provenance and scope, not invisible shared memory. A document retrieved from an untrusted source should not expand the permissions available to the executor. Local inference may help privacy or offline operation, but it does not make prompts, tools, or authorization automatically secure.
5.1 Token economics are change economics
An AI-assisted design may consume more tokens up front to establish use cases, ports, constraints, and tests. That is analogous to the initial design work in conventional development. The return comes later: when a model, API, storage provider, or SDK changes, an agent can work against a smaller, well-defined contract instead of repeatedly rediscovering and rewriting unrelated application behavior.
A lower token count for the first implementation is not necessarily a lower lifetime token cost. The saving depends on how often relevant details change and whether the boundary actually contains those changes. If the abstraction adds more context, maintenance, or verification than it saves, it has failed the same economic test as an unnecessary abstraction written without AI.
For a more complete treatment, see Threat Modeling AI as an Engineering Coprocessor.
6. Security, Testing, and Enforceable Conventions
Clean Architecture can make dependencies visible, but dependency boundaries are not automatically trust boundaries. An interface between packages in one process does not isolate secrets from code running with the same permissions. Security controls belong where identities, data, or authority actually cross trust boundaries.
Least privilege, complete mediation, defense in depth, fail-safe defaults, and economy of mechanism require implementation and evidence. The choice of a repository pattern or a use-case layer cannot replace authorization on a trusted backend.
Data lifecycles make this concrete. In transit, communication needs authenticated endpoints and protection appropriate to its sensitivity. At rest, access, retention, backups, and deletion matter. In use, cached credentials, analytics context, session transitions, and logs can expose data if ownership is unclear.
I favor Clean Architecture with explicit dependency injection because it makes application behavior testable without constructing an entire production environment. Use cases depend on narrow, application-owned ports; a composition root supplies the concrete filesystem, network, storage, or clock implementations. Tests can supply controlled in-memory implementations through the same constructor or function parameters. Dependency injection is the wiring mechanism; dependency inversion keeps that wiring from leaking infrastructure into policy.
In Invokrum’s composition tests, for example, a composition use case consumes an OverlaySource port. Tests inject a small MemorySource implementation to verify deterministic ordering, compatibility, resource limits, and failures without accessing the filesystem. The real source adapter still needs separate integration tests: a fake proves the use case’s behavior, not that the production adapter obeys the contract. This is why I prefer focused fakes over extensive interaction mocking, and explicit constructors over a dependency-injection framework introduced for its own sake.
The resulting testing strategy is a practical pyramid: unit tests for deterministic policy and state transitions, contract and integration tests for real adapters, and a smaller number of end-to-end tests for critical journeys such as authentication, synchronization, and recovery. Independent evidence of required behavior matters more than test counts or the number of mocks.
Conventions help enforce those decisions over time. A useful review asks whether policy imports infrastructure, whether a feature reaches into another feature’s private state, who owns a side effect, whether an API’s failures are defined, and what proves that the behavior survived the change. Package checks, type systems, static analysis, tests, and peer review can reinforce the conventions; none substitutes for engineering judgment.
7. Choosing Boundaries Worth Maintaining
Before introducing an abstraction, I would ask five practical questions:
- What changes independently? Identify a provider, policy, platform, workflow, or ownership concern likely to vary. Do not invent generality without an identifiable pressure.
- What contract remains stable? Define required inputs, outputs, errors, invariants, side effects, and responsibility for data. If these cannot be stated, the seam is probably premature.
- What can be verified separately? Decide which unit, integration, parity, security, or operational evidence will prove that an implementation honors the contract.
- Who will maintain the boundary? Make package ownership, review conventions, migration responsibilities, and knowledge transfer explicit. An architectural proposal without adoption capacity is only a proposal.
- Does the benefit exceed the cost? Include additional interfaces, runtime components, testing burden, complexity, infrastructure expense, and the ability to remove transitional code.
The answers often favor smaller interventions: isolate one provider, establish one source of state truth, make an unsafe action fail closed, or introduce a contract test before a migration. They can also justify a larger modularization when repeated changes demonstrate the need.
AI-generated design proposals also need to satisfy these criteria; suggesting another abstraction is not evidence that it is necessary.
Good architecture must remain understandable to the next engineer, not only to the person who designed it. Consistency, deliberate simplicity, and a safe path for incremental change often provide more value than an elaborate ideal structure.
8. Conclusion
The promise of software architecture is independence from changes that should not disturb essential behavior. Clean Architecture, SOLID, and design patterns provide useful vocabulary, but they become valuable only when the contracts are meaningful, the dependencies are enforceable, and the design survives contact with a running system.
Experience complicates the diagrams in productive ways. A structurally separate authentication path can still mishandle asynchronous state. A tested modernization prototype can remain unadopted. A model adapter can be replaceable while its outputs remain untrusted. A module interface can make security responsibilities clearer without enforcing them.
The best architecture is not the one with the most abstraction. It is the one that keeps useful policy under control, contains the cost of change, and makes correctness independently verifiable.
9. References
- Martin, R. C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. Prentice Hall, 2017.
- Martin, R. C. Writings on SOLID and software structure.
- Fowler, M. Humble Object.
- Original Medium edition, including its diagrams.
Related on This Blog
- Legacy Systems in the Real World
- AI SDLC: automating the grind
- Threat Modeling AI as an Engineering Coprocessor
- Modern Mobile Hardening
