In a modular PHP application, a boundary is more than a separation of folders, classes, or namespaces. It also determines what data a module can accept, what operations it can perform, and who is responsible for checking each condition. If these decisions remain implicit, modules may trust one another too much, duplicate validations, or expose data they should not control.
Designing trust boundaries between PHP modules means making these responsibilities explicit and keeping them verifiable as the system changes. It does not mean treating every internal call as hostile traffic or repeating every rule at every layer. It means identifying where authority changes, defining clear contracts, and validating each operation in the module that is responsible for performing it.
Why a boundary between modules is also a trust boundary

A module can consume data from another through an internal interface, an event, an API, or a shared query. In every case, it is worth asking what guarantees the sender provides and what the receiver needs to verify. Code location alone is not enough to answer this: a PHP method called directly may receive data derived from an external request, a background job, or another module that evolves independently.
Distinguish between format validation, business invariants, and authorization. Format validation checks that an identifier or date has an acceptable representation. An invariant ensures, for example, that an operation does not involve a negative amount. Authorization determines whether the actor can perform that operation on that resource. These controls are related, but they are not interchangeable.
A trust boundary becomes especially important when the actor, data ownership, or input channel changes. For example, a billing module may receive an order from another module, but it should not infer that the person who initiated the request has permission to modify any invoice. Identity and context must be provided explicitly and be verifiable.
Inventory the data and operations that cross each boundary
Start by mapping the relevant interactions, not by redesigning the entire application. For each boundary, document who calls, who owns the operation, what data is exchanged, and whether the call is synchronous or asynchronous. Include less visible paths: console commands, scheduled processes, queue consumers, and administrative tasks.
- Data: identify the fields transferred, their source, and whether they contain personal, sensitive, or other domain-specific information.
- Operations: record what changes are requested and which module must apply them.
- Context: determine which actor, organization, tenant, or purpose must accompany the request.
- Results: specify what information the module can return and what errors it can report.
This inventory helps uncover risk signals: a module queries tables owned by another module to avoid an interface; receives a resource identifier without checking ownership; or accepts a mutable domain object created by another module. It also reveals duplication: multiple consumers implement their own interpretation of a rule whose authority belongs to a single domain.
Define explicit contracts: format, invariants, and compatibility
A contract describes what the receiver needs to perform an operation and what guarantees it offers in response. In PHP, it can be expressed through immutable DTOs, interfaces, well-defined types, or message schemas, depending on the architecture. The specific form matters less than clarity: avoid passing entire entities when the consumer needs only a few values.
The contract must specify required fields, constraints, and meaning. A customerId, for example, does not by itself say whether it identifies a global customer, one belonging to an organization, or one visible to a particular user. If context affects the operation, include it or define how the responsible module obtains it.
Also agree on distinguishable errors. Malformed input is not the same as an unauthorized operation, a nonexistent resource, or a conflict with the current state. The details exposed to the caller should be sufficient to handle the result, without revealing data that could help enumerate resources or learn information about third parties.
Contracts should evolve with deliberate compatibility. Adding an optional field may be less disruptive than changing the meaning of an existing one, but it still requires reviewing producers and consumers. For asynchronous messages, consider that an older version may still be in circulation after new code is deployed. Do not confuse code deployment with enabling new behavior: use a controlled transition when producers and consumers do not change at the same time.
Validate authorization and ownership where the operation is performed
Syntactic validation can happen near the entry point, but authorization must be checked in the context that knows the resource and governs the action. The module that owns an invoice, for example, has the information needed to decide whether it can be issued or modified. It should not delegate that decision to a calling module that knows only the user’s identity.
A useful check links actor, action, resource, and context: who is requesting which operation, on which resource, and within which organization or tenant. Do not assume that a received identifier amounts to permission. Check ownership at the boundary that performs the operation, even if the request comes from internal code considered trusted. This prevents an alternative route or a later architectural change from bypassing authorization applied only in the web interface.
However, avoid replicating the entire policy across all modules. Centralize the decision in the responsible module and expose an operation or service with a clear contract. Consumer modules may perform convenience checks to improve the user experience, but they must not become the only security barrier.
Avoid letting shared entities create privileged access
Sharing ORM entities between modules may seem practical, but it can create hidden dependencies. If several modules load and modify the same entity, any of them may bypass the operations that should protect its invariants. In addition, schema or behavior changes affect consumers that do not control the model.
Prefer transferring the required data through commands, queries, or DTOs. The owning module remains responsible for persisting and validating its invariants; the consumer does not need general access to its repositories or tables. If a read is required, design a query that returns the minimum necessary data and respects the defined permissions and scope.
A shared database does not require shared authority. Even before physically separating the data, you can establish that only one module writes to specific tables and that the others use documented interfaces. This rule reduces the cost of a future separation and makes it easier to detect access outside the contract.
Test that isolation holds as the system changes
Tests should cover both the valid path and attempts to cross a boundary without permission. Unit tests check the owning module’s rules; integration tests verify contracts, persistence, and authorization configuration between components. Include cases involving a valid actor attempting to operate on a resource belonging to another tenant, a nonexistent resource, and an incomplete context.
- Verify that a valid identifier does not grant access on its own.
- Ensure that authorization errors do not return data from the protected resource.
- Test that consumers cannot modify invariants through shared entities or direct repository access.
- Run contract tests when producers and consumers are deployed independently.
Tests should reflect the actual policy, not merely check that an exception is thrown. Ensure that the change is not persisted, that no improper event is published, and that retrying the operation does not produce partial effects. For multi-step flows, define what happens if a step fails and how the state is reconciled.
Review boundaries when extracting a service or changing ownership

Extracting a separate service makes assumptions visible that were previously hidden in memory: identity, permissions, latency, retries, duplicates, and availability. The contract must carry sufficient context and account for network failures. Do not turn a remote call into a copy of a local call without reviewing timeouts, idempotency, and error handling.
Also review boundaries when responsibility for data changes, a tenant is introduced, a new channel is added, or new consumers appear. Ask who can write, who can read, which module decides authorization, and what guarantees remain in place during the migration. Keep these answers alongside the contract documentation and corresponding tests.
A useful trust boundary does not depend on all modules remaining in the same process or on the team remembering informal conventions. It is recognizable because authority is assigned, the data crossing it is limited, permissions are checked where the operation is performed, and tests detect unauthorized access. This protects isolation without turning every module into a replica of the others’ rules.



