Service Layer Standards
These are the rules for organizing business logic in this project. Follow them when creating or modifying services, use cases, domain rules, and any code that orchestrates between data access and API responses.
What Lives in the Service Layer
The service layer is the home for business logic — the rules and processes that make your application do what it does. It sits between the API layer (which handles HTTP) and the data layer (which handles persistence).
Belongs Here
- •Business rules and validation ("a user can't assign a task to themselves")
- •Orchestration ("to create a project, also create a default board and invite the owner")
- •Domain calculations ("the total invoice amount is sum of line items minus discount")
- •Complex operations that span multiple data models
- •External service integration logic (calling third-party APIs, sending emails)
Does NOT Belong Here
- •HTTP concerns (request parsing, response formatting, status codes) → API layer
- •Database concerns (SQL, ORM queries, migrations) → Data layer
- •UI concerns (formatting for display, pagination presentation) → Frontend
- •Configuration and environment concerns → Config layer
Organization
<!-- CUSTOMIZE: Replace with your conventions -->File Structure
- •One service file per domain concept:
UserService,TaskService,NotificationService - •Service files contain related functions, not a grab-bag of utilities
- •If a service file exceeds ~300 lines, it probably mixes two concerns — consider splitting
Naming
- •Services: named by domain concept (
UserService,BillingService) - •Methods: verb + noun describing the business action (
createProject,assignTask,calculateInvoiceTotal) - •Avoid generic names:
processData,handleStuff,doThing
Dependencies
- •Services depend on repositories/data layer — never directly on the database
- •Services may depend on other services, but watch for circular dependencies
- •External services (email, payments, file storage) are wrapped in their own service — business logic doesn't call APIs directly
Patterns
Function Signature
<!-- CUSTOMIZE: Adapt to your language conventions -->Every service function should be clear about:
- •What it needs (parameters — typed, validated)
- •What it returns (return type — explicit, not
any) - •What can go wrong (error types — documented, specific)
Error Handling
- •Throw domain-specific errors, not generic ones:
InsufficientBalanceError, notError("bad") - •Errors should carry context: "User 123 cannot be assigned to task 456 because they are not a project member"
- •Let the API layer translate domain errors to HTTP responses — services don't know about HTTP
- •Distinguish between:
- •Validation errors (bad input — the caller's problem)
- •Business rule violations (valid input but not allowed — domain logic)
- •System errors (infrastructure failure — not the caller's fault)
Transactions
- •The service layer owns transaction boundaries (not the API layer, not the data layer)
- •If an operation modifies multiple tables and must be atomic, wrap it in a transaction
- •If one step in a multi-step operation fails, roll back everything — no partial state
- •Keep transactions as short as possible — don't hold locks while calling external APIs
Side Effects
- •Separate the decision from the side effect: first compute what should happen, then make it happen
- •Side effects (sending emails, publishing events, calling external APIs) should be at the end of the function, after all validations pass
- •Consider making side effects async when they don't need to block the response
- •Side effects should be idempotent when possible — if the same operation runs twice, the result is the same
Business Rules
Where to Put Them
- •Simple validations on a single field → Model/schema level
- •Rules about a single entity → Service for that entity
- •Rules that span multiple entities → Service for the owning concept or a dedicated orchestration service
- •Rules that are used everywhere → Shared utility or domain helper
How to Express Them
- •Make business rules explicit, not buried in conditionals:
code
// BAD: Rule is hidden in an if statement if (user.role === 'admin' || (user.role === 'manager' && project.ownerId === user.id)) { ... } // GOOD: Rule is named and testable function canEditProject(user, project): boolean { ... } - •Business rules should be independently testable
- •When a rule changes, it should be clear where to change it (one place, not scattered)
External Service Integration
Wrapping External APIs
- •Every external service gets its own wrapper:
EmailService,PaymentService,StorageService - •The wrapper translates between the external API's interface and your domain's language
- •Business logic calls
emailService.sendTaskAssignment(user, task), neversmtp.send({...}) - •Wrappers handle: retries, timeouts, error translation, and logging
Resilience
- •External calls can fail — always handle the failure case
- •Decide per-integration: is failure blocking or non-blocking?
- •Blocking: payment must succeed before confirming an order
- •Non-blocking: failure to send a notification shouldn't fail the whole operation
- •Use timeouts — never wait indefinitely for an external service
- •Log external call failures with enough context to debug later
Testing
What to Test
- •Business rules: "when [condition], then [outcome]"
- •Orchestration: "when creating a project, it also creates a board and sends an invitation"
- •Error cases: "when the user doesn't have permission, it throws [specific error]"
- •Edge cases: boundary values, null handling, concurrent modifications
How to Test
- •Mock the data layer — test business logic, not database queries
- •Mock external services — test integration logic, not third-party APIs
- •Don't mock other services in the same domain unless testing in isolation is genuinely necessary
- •Use descriptive test names: "should reject assignment when user is not a project member"
Rules
- •Every business rule should have at least one test
- •Test the happy path AND the rejection path for every rule
- •Integration tests (real database, real services) are valuable but separate from unit tests
- •Keep tests fast — if a test needs a database, it's an integration test, not a unit test