A backend organized into feature folders can still become tightly coupled. If the courses feature imports the notification repository and writes the streak collection, folder names provide appearance rather than boundaries.
I assign one owner to each business truth. Users own profile data. Progress owns completion facts. Notifications own delivery. Other features interact through a small public capability or a published event.
Use a port for a required answer#
When an operation needs an immediate result, depend on a narrow interface:
export interface MembershipReader {
getMembership(input: {
userId: string;
organizationId: string;
}): Promise<Membership | null>;
}
The consuming feature knows the information it needs, not the owning feature's collection schema. Wiring connects the port to an adapter.
Use an event for a fact others may react to#
After a course is completed, publish course.completed. Milestones and analytics can react independently. The course use case should not import both implementations.
Events are unsuitable when the caller needs an immediate answer to finish correctly. Do not replace a simple permission lookup with asynchronous messaging merely to claim the system is event-driven.
Keep shared code small#
Shared platform modules should contain stable technical policy: logging, authentication primitives, database connection setup, and event envelopes. Shared business helpers often become a hidden feature with no owner.
If several features need a concept such as membership, expose it from the feature that owns membership rather than copying its queries into a common folder.
Enforce the direction#
Document allowed imports and add lint rules where practical. Code review alone will eventually miss a convenient cross-feature repository import.
Test public contracts and events. Internal refactors should not affect consumers, while a contract change should fail clearly.
Strong boundaries do not require separate services. A modular backend can deploy together while preserving ownership in code. That gives a team room to change internals today and split deployment units later only if operations justify it.