business-logic-entry-point-prefer-top-level-functions
Prefer Top-Level Functions for Business Logic Entry Points
Goal
When implementing a business-logic entry point, prefer a top-level function over a class or object, as long as the project stack allows it without introducing friction.
A top-level function is a function defined at module scope, not inside a class or object. It is the simplest code shape for an entry point: one function, one module, no wrapper.
What Counts as In Scope
Apply this skill to code that does one or more of these things:
- defines a new business-logic entry point and must choose between a top-level function, a class, or an object
- wraps a single entry-point function in a class or object without a clear reason
- uses a class or object only as a container for one public method plus dependency injection that could be achieved with function parameters or closures
The Rule
-
Default to a top-level function for each business-logic entry point.
- Export the function as the module's single public entry point.
- Pass dependencies as function parameters, closures, or through the module's imports, depending on the language's idiomatic patterns.
-
Use a class or object only when the project stack makes it the natural choice.
- The language requires classes for dependency injection and there is no idiomatic function-based alternative.
- The framework expects classes or objects for registration, routing, or lifecycle management.
- The project already uses classes consistently for entry points and switching to functions would introduce inconsistency or friction.
-
Do not wrap a function in a class just for the sake of having a class.
- A class with one public method, a constructor that only assigns dependencies, and no meaningful state is a function in disguise.
- If the language supports top-level functions and the framework does not require a class, use a function.
When Classes Are Appropriate
Classes or objects are appropriate when:
- The language does not support top-level functions as first-class module exports (e.g., Java without workarounds).
- The dependency injection framework requires class-based registration and there is no idiomatic function adapter.
- The project has an established convention of using classes for entry points and the team has decided to keep that convention.
- The entry point genuinely manages internal state across its lifecycle, beyond simple dependency references.
In these cases, use a class or object with a single public method, following the one-entry-point-per-module rule.
Examples
Prefer this when the stack allows it:
// create-reservation-command-handler.ts
export function createReservationCommandHandler(
command: CreateReservationCommand,
): ResultAsync<CreateReservationCommandHandlerSuccess, CreateReservationCommandHandlerError> {
// business logic
}
# create_reservation_command_handler.py
def create_reservation_command_handler(
command: CreateReservationCommand,
) -> CreateReservationCommandHandlerSuccess:
# business logic
// CreateReservationCommandHandler.kt
fun createReservationCommandHandler(
command: CreateReservationCommand,
): CreateReservationCommandHandlerSuccess {
// business logic
}
Avoid this when a top-level function would work:
// create-reservation-command-handler.ts
class CreateReservationCommandHandler {
constructor(private readonly reservationRepository: ReservationRepository) {}
execute(command: CreateReservationCommand): ResultAsync<...> {
// business logic
}
}
Use a class when the stack requires it:
// CreateReservationCommandHandler.java
public class CreateReservationCommandHandler {
private final ReservationRepository reservationRepository;
public CreateReservationCommandHandler(ReservationRepository reservationRepository) {
this.reservationRepository = reservationRepository;
}
public CreateReservationCommandHandlerSuccess execute(CreateReservationCommand command) {
// business logic
}
}
Detection Workflow
-
Check the project stack.
- Determine whether the language supports top-level functions as first-class exports.
- Determine whether the framework or dependency injection system requires classes.
-
Inspect existing entry points.
- Check whether the project uses classes or top-level functions for business-logic entry points.
- If the project consistently uses classes and there is no initiative to change, respect that convention.
-
Identify unnecessary class wrappers.
- Look for classes with one public method, a constructor that only assigns dependencies, and no meaningful internal state.
- These are candidates for conversion to top-level functions if the stack allows it.
Writing or Changing Entry Points
-
Check the stack first.
- If top-level functions are idiomatic and frictionless, use a top-level function.
- If the stack requires or strongly favors classes, use a class with one public method.
-
Pass dependencies explicitly.
- For top-level functions, pass dependencies as parameters, use closures, or import them at module level, depending on the language's conventions.
- For classes, inject dependencies through the constructor.
-
Do not introduce a class solely for future extensibility.
- A function can be refactored into a class later if the need arises.
- Start with the simpler shape.
Review Questions
When reading or reviewing code, ask:
- Is this entry point implemented as a class when a top-level function would work?
- Does the class have only one public method and a constructor that assigns dependencies?
- Does the project stack require or strongly favor classes for this purpose?
- Would switching to a top-level function introduce friction or inconsistency?
If a class is used without a clear reason and the stack supports top-level functions, apply this skill.
Report the Outcome
When finishing the task:
- state which entry points were implemented or changed
- state whether top-level functions or classes were used and why
- state whether the project stack influenced the choice
More from code-sherpas/agent-skills
neverthrow-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.
7update-agent-skills
Update agent skills installed with the `skills` CLI. Use when asked to refresh installed skills, keep a project's skills current, or troubleshoot cases where `npx skills update` reports that everything is up to date. For project-scoped installs, a no-change update must immediately run the bundled reinstall script so tracked skills from `skills-lock.json` are reinstalled without extra investigation.
7