Independent Deployment ≠ Clean Internals: Isolating Business Rules from Technical Details

This article explains why independently deployed services can still have chaotic internal architecture, and demonstrates how to separate business rules, use cases, ports, and adapters to ensure dependencies point inward, enabling testable, evolvable systems without mechanically applying layered templates.

Data Bricklaying Diary
Data Bricklaying Diary
Data Bricklaying Diary
Independent Deployment ≠ Clean Internals: Isolating Business Rules from Technical Details

The previous article discussed service decomposition focusing on business boundaries, data ownership, and team responsibility. However, even after services are split and independently deployed, their internals often remain a tangled mess. A typical "refund service" may have its own interface and database, yet its controller mixes order validation, refund state changes, payment SDK calls, and messaging; ORM entities leak into API responses; and channel timeouts, database schema changes, or message format adjustments permeate core business logic. Physical boundaries exist, but internal architectural boundaries do not.

Two Candidate Approaches

For a refund service with moderate complexity, two structural options exist:

Direct Transaction Script – suitable when rules are simple, external side effects few, and lifecycle short. Benefit: short code paths, low delivery and debugging cost. Risk: accumulated changes bind rules to frameworks and external calls.

Isolate Business Rules and Adapters – suitable when state machines, external channels, auditing, compensation, or audit trails create complexity. Benefit: changes confined to boundaries, key rules independently verifiable. Risk: added translation and abstraction overhead, requires ongoing boundary maintenance.

The article focuses on the second approach, noting the first remains valid when its conditions match.

Separate Strategy from Mechanism

The core of internal service architecture is decoupling stable business strategy from faster-changing implementation mechanisms; directory layering is only one possible form. In the refund example, four categories emerge:

Business Strategy : refund eligibility, amount rules, state transitions, compensation conditions – changes driven by business policy and risk rules.

Business Use Cases : create application, review, submit to channel, query progress, manual handling – changes driven by process and role boundaries.

Access Mechanisms : HTTP interfaces, background tasks, message subscriptions, admin pages – changes driven by callers and interaction styles.

External Mechanisms : databases, payment channels, messaging platforms, object storage – changes driven by technology choices, vendors, and runtime environments.

A business use case is not a controller. "Submit refund application" can be invoked via HTTP, batch job, bulk import, or admin backend; the controller merely translates external requests into use-case inputs and results into HTTP responses. Likewise, "payment channel refund" is not a domain rule; SDK details, signatures, and network exceptions belong in an external adapter, while retry allowance, stop conditions, and escalation to manual handling – involving fund side effects – remain decisions of the refund use case and business rules.

Executable Internal Structure: Entities, Use Cases, Ports, Adapters

For services with sufficient complexity, organize around four responsibilities:

Business Entities hold refund state, amount, eligibility, and invariants. They need not be a single class named RefundEntity; they can be data structures and functions, provided key business data and rules stay together and remain ignorant of databases, UIs, and third-party frameworks.

Business Use Cases orchestrate a single business action.

Ports define the capabilities a use case requires – input/output contracts corresponding to Clean Architecture's input boundaries, output boundaries, and data-access interfaces, without rigid naming.

Adapters implement ports using concrete technologies (databases, HTTP, messaging, SDKs).

For example, the "Submit Refund Application" use case should not depend directly on a PaymentSdk. It declares a "Submit Channel Refund" port; a payment adapter implements that port with the actual SDK. The use case sees only business-level results already translated by the adapter. Similarly, a "Refund Record Repository" port isolates ORM usage; ORM fields, lazy-loading behavior, and query semantics cannot dictate business rules.

Business defines ports, technology implements adapters
Business defines ports, technology implements adapters

This chain does not mandate asynchronous calls or separate services for every action; it only requires each layer to own its responsibility: use cases decide what to do, adapters decide how to integrate specific technology.

Dependency Direction Inward, Not Toward Frameworks

Source-code dependency direction is the real key. If refund rules import web frameworks, ORM annotations, message clients, or payment SDKs, business code changes with those details. Conversely, when web, database, and SDK adapters depend on ports defined by business use cases, technical details are confined outside the boundary; swapping technology then affects adapters first, not core business.

Runtime calls outward, source dependencies inward
Runtime calls outward, source dependencies inward

Arrows represent compile-time dependencies, not runtime data flow. At runtime, use cases call databases and payment channels through ports; at compile time, ports are defined by the business side and implemented by adapters. This is the practical meaning of Clean Architecture's dependency rule inside a service: not to write more interfaces for their own sake, but to let business rules dictate required capabilities instead of being shaped by a framework or vendor API.

Not every concrete class needs inversion. Implementations that never enter core business rules and require no independent replacement or testing need no artificial interface wrapper. The true isolation targets are payment SDKs, ORM mappings, message clients, and external protocols – things that change easily and tend to permeate business code.

Architecture Boundaries as Reviewable Program Design

An architecture diagram shows boundaries but not the details: what inputs/outputs the "create application" use case expects, how unknown channel results are represented, which use case may change state, or who publishes a message. These belong to program design – key types, port signatures, state transitions, and call relationships. Before development, clarify at least: business fields in the application command and result; business results returned by order-read, refund-save, and channel-submit ports; conditions for state transitions from "accepted" to "processing" or "pending manual"; and which layer handles channel exceptions, duplicate requests, and fact publishing.

The goal is not to document every private method but to eliminate ambiguities that could alter business rules, dependency direction, or recovery behavior, so developers and reviewers share the same boundaries rather than each implementer filling gaps according to framework habits.

Implementation should not proceed by stacking table structures, service layers, interfaces, and pages separately then integrating. A safer approach is a vertical slice along the minimal refund path: first get the interface contract, use case, port stubs, and state transitions running; then incrementally integrate database, payment channel, and messaging. Each completed full path allows checking model conversion, dependency direction, and failure handling against the design; deviations are caught when the modification scope is still small.

Concrete Implementations Concentrated at the Periphery

Database adapters still use ORM or SQL; payment adapters still call SDKs; the application must still instantiate and inject these implementations at startup. The goal is not to make concrete implementations disappear but to concentrate them in a few peripheral locations. The composition root (application entry) creates payment, database, and message adapters, binds ports to implementations, and starts web, task, and message entry points. Business entities and use cases depend only on their own rules and required ports, with no knowledge of Spring, DI containers, or how specific clients start.

Composition root assembles concrete implementations
Composition root assembles concrete implementations

Three Models – Don't Merge Them for Convenience

Another common internal chaos: a single Java entity or database record serves simultaneously as interface parameter, business object, persistence object, and message body. Short-term it saves conversion classes; long-term any change drags all three along.

In the refund service, at least three distinct models are needed:

Business Model – for business rules and use cases; must not carry HTTP fields, ORM lifecycle, or channel protocol details.

Persistence Model – for database reads/writes and index design; must not carry business judgments or interface compatibility promises.

Exchange Model – for HTTP, messaging, and external channels; must not carry core state machines or cross-use-case rules.

Business, persistence, and exchange models serve different boundaries
Business, persistence, and exchange models serve different boundaries

For instance, a payment channel may represent results as SUCCESS, PROCESSING, UNKNOWN protocol states, while the refund business model expresses "accepted, processing, success, pending manual". The two do not map one-to-one and should not share an enum. Model conversion adds code but confines changes at boundaries: channel protocol adjustments hit the channel adapter; database optimizations hit the persistence adapter; interface version evolution hits the access adapter. The business state machine is not forced to rewrite.

Testability Is Not an Afterthought

If refund eligibility, amount rules, and state transitions can only be verified after starting a web container, connecting a database, and mocking a payment channel, business rules are still bound by technical mechanisms. A better approach is layered verification:

Business rule tests directly check amounts, states, and invariants.

Use case tests use port stubs to verify idempotency, failure branches, and fact publishing.

Adapter tests validate ORM mappings, payment protocols, and message conversions.

End-to-end tests confirm key call chains work in near-real environments.

Different failures need different test evidence levels
Different failures need different test evidence levels

This does not require all tests to use mocks. Critical dependencies like databases, messaging, and payment sandboxes still need integration or contract tests. The key is that different layer failures are verified by different evidence; a single interface integration run cannot declare the service reliable.

For the refund service, a concrete verification matrix emerges:

Refund rules independent of technical mechanisms – rule tests need no web, database, or payment SDK (unit tests, dependency checks).

Technical changes stop at boundaries – use cases depend only on ports, not concrete clients (architecture tests, code reviews).

Architecture boundaries reflected in implementation – key types, port signatures, state transitions do not bypass business rules (design reviews, architecture tests, code reviews).

Protocol changes confined to boundaries – channel field changes don't modify core state machine (adapter tests, change logs).

State and side effects verifiable – duplicate requests, unknown channel results have clear conclusions (use case tests, integration tests, reconciliation records).

Interfaces and data evolvable – version changes don't require core business rewrites (contract tests, release records).

Passing tests only prove covered behaviors hold. Without real-channel, real-traffic, and fault-drill evidence, production stability cannot be claimed.

Don't Treat Clean Architecture as a Fixed Template

Seeing entities, use cases, ports, and adapters, many teams immediately create four-layer directories and add an interface for every class, ending up with excessive forwarding code and untraceable call chains. The article opposes this mechanical layering.

Clean Architecture is not a four-layer directory generator
Clean Architecture is not a four-layer directory generator

The concentric circles in Clean Architecture illustrate the dependency rule; they are not a mandatory four-layer directory for every service. Internal boundary granularity should be driven by business complexity. A simple query interface with no external side effects may be better served by a clear query service and persistence implementation than by full layering. A refund capability involving state machines, fund channels, auditing, compensation, and audit trails warrants separating business rules from external mechanisms.

Whether to add internal boundaries can be decided by three questions:

Will this business rule be forced to change because of a framework, table structure, or vendor SDK change?

Can this key business path be verified without spinning up all infrastructure?

Can the added interfaces or abstractions isolate real changes, rather than just adding another forwarding layer?

If rules are simple, infrastructure changes don't affect them, and direct testing is clear, don't add abstractions for the sake of layering completeness. Conversely, if key rules are repeatedly dragged by external mechanisms, prioritize pulling those changes back to the boundary.

Summary

After services are split, internal boundaries are still needed. Business rules and use cases describe business decisions; ports express the capabilities those decisions require; HTTP, databases, messaging, and SDKs enter through adapters. This is done so that framework, schema, and vendor protocol changes stop at the periphery – not to pursue surface-level complex layering.

1. First let business rules decide what capabilities they need,
2. Then let technical mechanisms implement those capabilities at the boundary.

The next article will discuss dynamic collaboration between services: even when each service's internal boundaries are clear, dependencies, routing, version compatibility, and release practices still determine whether they can truly evolve independently.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

clean architecturedependency inversionservice decompositionlayered testingports and adaptersmodel separationbusiness rules isolationinternal architecture
Data Bricklaying Diary
Written by

Data Bricklaying Diary

Records practices, thoughts, and pitfalls on the data grunt-work journey, sharing content on data platforms, data analysis, data processing, data governance, knowledge graphs, and more. Less theory, more hands‑on, making complex data technologies simple.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.