business-logic-entry-point-one-per-module
One Entry Point per Module for Business Logic
Goal
Every business-logic entry point must be a separate module. Do not group multiple entry points together in the same class, object, or file because they operate on the same domain entity type.
A module here means the unit of code organization provided by the language: a file, a single-class file, a single-object file, or equivalent. Each entry point gets its own.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines multiple business-logic entry points in a single class, object, or file
- groups command handlers, query handlers, use cases, or application services by domain entity type into a shared container
- introduces a service class or facade that bundles several business operations as methods on one object
- organizes business-logic entry points by entity rather than by operation
The Rule
-
Each business-logic entry point must live in its own module.
- One command handler per module.
- One query handler per module.
- One use case per module.
- One application service method becomes one application service module with a single public function.
-
Do not group entry points by domain entity type.
- Do not create a
UserServiceclass withcreateUser,updateUser,findUserById, anddeleteUserByIdas methods. - Do not create a
ReservationUseCasesclass that bundles all reservation-related operations. - Do not create a file that exports multiple entry-point functions for the same entity.
- Do not create a
-
Each module exposes exactly one business-logic entry point.
- If the entry point is a top-level function, the module contains that function as its single public entry point.
- If the entry point is a class or object, it must have exactly one public function that acts as the entry point. Construction, dependency injection, and private helpers do not count as additional public entry points.
-
Name the module after the entry point.
- The file or module name should reflect the specific business operation, not the domain entity type.
- Prefer names like
create-reservation-command-handler,find-user-by-id-query-handler, or the local equivalent over names likeuser-service,reservation-use-cases, ororder-handlers.
Why Not Group by Entity
Grouping entry points by domain entity creates classes or files that grow with every new operation. This leads to:
- large files with unrelated operations sharing scope and dependencies
- entry points that accumulate shared private state or helpers that blur their boundaries
- difficulty isolating one operation's dependencies from another's
- merging conflicts when different developers work on different operations for the same entity
Keeping each entry point in its own module makes each operation self-contained, with its own explicit dependencies and no accidental coupling to sibling operations.
Detection Workflow
-
Find classes, objects, or files that contain multiple business-logic entry points.
- Look for service classes, use-case bundles, handler collections, or files that export several entry-point functions.
- Check whether the grouping criterion is the domain entity type.
-
Count the public entry points per module.
- If a class or object has more than one public function that acts as a business-logic entry point, it violates this rule.
- If a file exports more than one entry-point function, it violates this rule.
-
Check the module name.
- If the module is named after a domain entity type rather than a specific operation, it likely groups multiple entry points.
Writing or Changing Entry Points
-
Create a new module for each new entry point.
- Do not add a new method to an existing entity-grouped class.
- Create a new file with the entry point as its single public operation.
-
When refactoring, extract grouped entry points into separate modules.
- Move each method from an entity-grouped class into its own module.
- Give each module its own explicit dependencies.
-
Keep private helpers local to the entry point that uses them.
- If a helper is used by only one entry point, keep it in that entry point's module.
- If a helper is shared across multiple entry points, extract it into its own module rather than keeping it as a shared private method in a grouped class.
Examples
Avoid this:
// user-service.ts
class UserService {
createUser(command: CreateUserCommand): Promise<CreateUserCommandHandlerSuccess> { ... }
updateUser(command: UpdateUserCommand): Promise<void> { ... }
findUserById(query: FindUserByIdQuery): Promise<FindUserByIdQueryHandlerSuccess> { ... }
deleteUserById(command: DeleteUserByIdCommand): Promise<void> { ... }
}
Prefer this:
create-user-command-handler.ts
update-user-command-handler.ts
find-user-by-id-query-handler.ts
delete-user-by-id-command-handler.ts
Each file contains exactly one entry point.
Review Questions
When reading or reviewing code, ask:
- Does this module contain more than one business-logic entry point?
- Are multiple entry points grouped because they operate on the same domain entity type?
- Does the module name refer to a domain entity type rather than a specific business operation?
- Would adding a new operation for this entity require modifying this module?
If the answer is yes, apply this skill.
Report the Outcome
When finishing the task:
- state which entry points were identified or separated into their own modules
- state which entity-grouped classes or files were split
- state how each module was named after its specific business operation
More from code-sherpas/agent-skills
neverthrow-return-types
Require `neverthrow`-based return types in TypeScript and JavaScript code whenever the surrounding technology allows it. Use when creating, refactoring, reviewing, or extending standalone functions, exported module functions, class methods, object methods, service methods, repository methods, and similar APIs that should expose explicit success and failure result types in their signatures. Prefer `Result<T, E>` for synchronous code and `ResultAsync<T, E>` for asynchronous code. Only skip a `neverthrow` return type when a framework, library, runtime interface, or externally imposed contract is incompatible and requires a different return shape.
16neverthrow-wrap-exceptions
Capture exceptions and promise failures with `neverthrow` instead of hand-written `try/catch` in TypeScript and JavaScript code. Use when wrapping synchronous functions that may throw, promise-returning functions that may throw before returning, existing `PromiseLike` values that may reject, or third-party APIs such as parsers, database clients, HTTP clients, file-system helpers, serializers, and SDK calls. Prefer `Result.fromThrowable` for synchronous throwers, `ResultAsync.fromThrowable` for promise-returning functions that may throw or reject, and `ResultAsync.fromPromise` when you already have a `PromiseLike` value in hand. Only keep `try/catch` when the language construct, cleanup requirement, or framework boundary truly requires it.
11atomic-design
Create or update web UI components with a strict reuse-first workflow. Use when building, refactoring, restyling, or extending frontend or template components while minimizing raw DOM or HTML by reusing or generalizing existing components first.
10write-persistence-representations
Create or update persistence-layer data representations in any stack, including ORM entities, schema definitions, table mappings, document models, collection definitions, and similar database-facing code. Use when agents needs to add or change persisted fields, identifiers, relationships, indexes, timestamps, auditing fields, or storage mappings in frameworks, libraries, or ORMs such as Prisma, TypeORM, Sequelize, Drizzle, Mongoose, Hibernate/JPA, Doctrine, Ecto, Active Record, or equivalent persistence technologies.
7business-logic
Identify, interpret, review, or write business logic in code. Use when an agent needs to decide whether code expresses business rules, business algorithms, or business workflows, or when it must implement, preserve, or refactor code that creates, stores, or transforms data according to real business policies.
7immutable-domain-entities
Require the immutable design pattern for domain entities. Use when an agent needs to create, modify, review, or interpret domain entities and should preserve identity while expressing state changes through new immutable instances. Domain entities must be modeled as immutable classes, not as plain type aliases or interfaces paired with standalone functions.
7