Infrastructure Concerns in the Business Layer
When infrastructure details leak into the application and domain layers — making business logic complex, fragile, and hard to reuse.
The Principle
The application and domain layers should be infrastructure-transparent. They express what the business does, in business terms. How data is stored, how it is encrypted, how it is transported — these are infrastructure concerns, and they belong in the infrastructure layer.
When infrastructure concerns leak inward, they change the shape of the business interface. Callers of business logic — other services, background workers, event handlers, tests — must now deal with infrastructure details that have nothing to do with what they are trying to accomplish. The business becomes harder to call, harder to test, and fragile to infrastructure changes.
Manifestation 1: Infrastructure Context in Service Signatures
The clearest form: an infrastructure concern that could live at the persistence boundary is instead handled in the application layer, polluting every method signature and constructor.
Consider a service responsible for managing profiles, where sensitive fields must be encrypted at rest. There are two integration points available:
Option A — Encryption in the application layer:
class ProfileService:
def __init__(
self,
profile_repository: IProfileRepository,
encryption_service: EncryptionService, # ← infrastructure dependency
encryption_context: EncryptionContext, # ← infrastructure state
):
...
async def update_profile(self, user_id: UUID, updates: ProfileUpdatesPlainDTO) -> ProfilePlainDTO:
encrypted = await self.encryption_service.encrypt(updates, context=self.encryption_context)
result = await self.profile_repository.update(user_id, encrypted)
return await self.encryption_service.decrypt(result)Every caller of ProfileService now knows that encryption exists. A background worker updating a profile must obtain and pass an EncryptionContext. An event handler triggering the same operation has the same requirement. Adding any new consumer of this business logic means wiring up encryption infrastructure, even if the new consumer has no direct interest in how data is stored.
Option B — Encryption at the infrastructure boundary:
class ProfileService:
def __init__(
self,
profile_repository: IProfileRepository, # ← only the business dependency
):
...
async def update_profile(self, user_id: UUID, updates: ProfileUpdates) -> Profile:
profile = await self._profile_repository.find_by_id(user_id)
profile.apply_updates(updates)
await self._profile_repository.save(profile)
return profileProfileService knows nothing about encryption. The SQLAlchemy repository adapter handles it in its mapper — the domain object arrives plaintext, the ORM record is encrypted, and on retrieval the decrypt happens before the domain object is returned.
A new consumer of ProfileService needs no knowledge of encryption. Tests can use an in-memory adapter that stores plaintext. The encryption implementation can be replaced without touching business logic.
The correct integration point for infrastructure concerns is the adapter — the infrastructure-layer implementation of a domain-defined port.
Manifestation 2: API Transport Objects in Service Signatures
A related form, with a different layer leaking in the wrong direction. The API layer defines request and response models shaped for its HTTP contract: optional fields for partial updates, string representations of IDs, field names that match the client's expectations. When these objects appear in application layer method signatures, the API layer's concerns become the application layer's concerns.
# API-layer model leaking into the application layer
class AccountService:
async def create_account(
self,
request: CreateAccountRequest, # ← Pydantic HTTP request model
) -> AccountResponse: # ← Pydantic HTTP response model
...The consequences:
- Every consumer must speak HTTP. A background worker that wants to create an account must construct a
CreateAccountRequest— an object designed for HTTP deserialisation — to call business logic. - API changes break service interfaces. A new field added to the API model for a frontend requirement changes the service signature, even if the business logic is unchanged.
- Tests couple to the API layer. Unit tests of application logic must import from the API layer.
- Validation semantics mix.
CreateAccountRequestvalidates for HTTP correctness. The domain has its own invariants. They are now entangled.
The correct pattern: the API layer translates the request into a command or DTO that speaks the domain's language, passes it inward, and maps the result back to an HTTP response at the boundary.
# API layer: translates in both directions, nothing leaks
@router.post("/accounts")
async def create_account(
request: CreateAccountRequest,
handler: CreateAccountHandler = Depends(get_handler),
):
command = CreateAccount( # ← domain command, not HTTP model
email=request.email,
name=request.name,
)
account = await handler.handle(command)
return AccountResponse.from_domain(account) # ← translation back to HTTP
# Application layer: speaks only domain language
class CreateAccountHandler:
async def handle(self, command: CreateAccount) -> Account:
account = Account.create(command.email, command.name)
await self._accounts.save(account)
return accountThe General Principle
Both manifestations share the same root: an outer layer's concerns are crossing their integration point and leaking into an inner layer.
Infrastructure concerns (encryption, serialisation, storage mechanics, transport format) belong in the infrastructure or API layer. When they cross into the application or domain layers, the business logic becomes entangled with implementation details that are irrelevant to the domain.
A useful test: would a background job, an event handler, or an in-memory test have to deal with this concern? If the answer is no, it belongs at the adapter — not in the business layer.
Read Next
- Hexagonal Architecture — Ports & adapters: how the adapter pattern enforces the correct integration point for infrastructure concerns
- Layered Architecture — Layer responsibilities and what each layer should and should not know about
- Dependency Direction Violations — The outward version of the same problem: outer layers absorbing inner layer logic