Create Service Object
HARD-GATE
TESTS GATE IMPLEMENTATION:
EVERY service object MUST have its test written and validated BEFORE implementation.
1. Write the spec/test for .call (with contexts for success, error, edge cases)
2. Run the spec/test — verify it fails because the service does not exist yet
3. ONLY THEN write the service implementation
The final artifact must include the test command and the failure message
before implementation. Use the observed failure when available; otherwise show
the exact expected failure class/message for the missing service.
See tdd-process for the full gate cycle.
Core Process
- Write Spec (Test-First): Create the spec/test file at
spec/services/<module_name>/<service_name>_spec.rb (or test/services/). Cover success and error paths for .call. Run it to confirm it fails (see HARD-GATE). Tests must assert success: and response: top-level keys and the meaningful payload shape.
- Define Service Skeleton: Create
services/<module_name>/<service_name>.rb with the correct module namespace.
- Select Pattern: Choose Standard, Batch, Class-only (Pattern 3), or Orchestrator based on requirements. State whether instance state is required — if not, use Pattern 3 (no
initialize, no instance variables).
- Implement Contract: Implement
self.call and #call. Response must always be { success: true, response: { ... } } or { success: false, response: { error: { message: '...' } } }. Keep call ≤ 20 lines; extract sub-services if longer. Validate inputs at top of call; return error hash if invalid. Return serialized data only — no raw persistence model objects (e.g. ActiveRecord, ROM) in response.
- Handle Errors and Logging: Catch
StandardError (and domain exceptions). Log with the application logger (e.g., logger.error). Use UPPER_SNAKE_CASE constants for all user-facing error strings — never inline in a rescue. Never re-raise to caller.
- Add YARD Documentation: Add
@param, @return [Hash], and @raise tags to self.call and every other public method. Document self.call separately from #call. For class-only services (Pattern 3), if the class returns a non-standard shape (e.g. nil / error string), document that explicitly in YARD and the README.
- Write Module README: Generate
services/<module_name>/README.md explaining domain context. Required even for single-service modules.
Additional Constraints
| Aspect |
Rule |
| Transactions |
Only wrap multi-step database operations that must be atomic |
| Scope |
Return data only (no HTTP/UI concerns); single responsibility per service |
| SQL |
Use query sanitization for any dynamic queries |
| Shared logic |
Extract validators to class-only services (Pattern 3) |
Core Patterns
1. The .call Pattern
def self.call(params)
new(params).call
end
def call
# ... processing ...
{ success: true, response: { data: result } }
rescue StandardError => e
logger.error("Processing Error: #{e.message}")
logger.error(e.backtrace.join("\n"))
{ success: false, response: { error: { message: ERROR_MESSAGE } } }
end
2. Batch Processing + Per-Item Rescue (Partial Success)
def call
results = @items.each_with_object({ successful: [], failed: [] }) do |item, acc|
# process...
rescue StandardError => e
logger.error("Unexpected item error: #{e.message}")
acc[:failed] << { sku: item[:sku], error: e.message }
end
{ success: true, response: results }
end
3. Class-only Services (Static Methods)
When no instance state is needed, use ONLY class methods — no initialize, no instance variables. Suitable for validators, formatters, and argument-only helpers.
class Orders::QuantityValidator
def self.call(quantity:)
return { success: false, response: { error: { message: INVALID_QUANTITY } } } unless quantity.positive?
{ success: true, response: { valid: true } }
end
end
4. Orchestrator Delegation (≤20-line call)
def call
user_result = UserCreationService.call(@params)
return user_result unless user_result[:success]
# ... continue ...
end
Extended Resources (Progressive Disclosure)
Load these files only when their specific content is needed:
- assets/examples.md — Detailed examples of the 4 core patterns (Standard, Batch, Static, Orchestrator).
- assets/service_skeleton.md — Basic starting skeleton.
- assets/module_readme_template.md — Template for the mandatory module README.
Integration
| Skill |
When to chain |
| write-yard-docs |
Writing/reviewing inline docs |
| integrate-api-client |
External API integrations |
| implement-calculator-pattern |
Variant-based calculators |
| write-tests |
General testing structure |
| refactor-process |
Refactoring service objects |
1---2name: create-service-object3description: Use when creating or refactoring Ruby service classes following the `def self.call(...)` → `new(...).call` entry point pattern with a strict `{ success: true/false, response: { ... } }` response contract. Handles error shape (`{ success: false, response: { error: { message: string } } }`), `StandardError` rescue with `logger.error` logging, `UPPER_SNAKE_CASE` error constants, and mandatory module READMEs. Enforces test-first workflow: spec written and confirmed failing before implementation. Covers 4 core patterns (Standard, Batch, Static/Class-only, Orchestrator), `.call` ≤ 20 lines, and YARD documentation on `self.call` and `#call`. File layout: spec at `spec/services/[module]/[name]_spec.rb`, impl at `services/[module]/[name].rb`. Trigger words: service object, .call pattern, services, service module, response hash, success/response shape, YARD on self.call, service skeleton, module README, orchestrator.4license: MIT5---6# Create Service Object78## HARD-GATE910```text11TESTS GATE IMPLEMENTATION:12EVERY service object MUST have its test written and validated BEFORE implementation.13 1. Write the spec/test for .call (with contexts for success, error, edge cases)14 2. Run the spec/test — verify it fails because the service does not exist yet15 3. ONLY THEN write the service implementation16The final artifact must include the test command and the failure message17before implementation. Use the observed failure when available; otherwise show18the exact expected failure class/message for the missing service.19See tdd-process for the full gate cycle.20```2122## Core Process23241. **Write Spec (Test-First):** Create the spec/test file at `spec/services/<module_name>/<service_name>_spec.rb` (or `test/services/`). Cover success and error paths for `.call`. Run it to confirm it fails (see HARD-GATE). Tests must assert `success:` and `response:` top-level keys and the meaningful payload shape.252. **Define Service Skeleton:** Create `services/<module_name>/<service_name>.rb` with the correct module namespace.263. **Select Pattern:** Choose Standard, Batch, Class-only (Pattern 3), or Orchestrator based on requirements. State whether instance state is required — if not, use Pattern 3 (no `initialize`, no instance variables).274. **Implement Contract:** Implement `self.call` and `#call`. Response must always be `{ success: true, response: { ... } }` or `{ success: false, response: { error: { message: '...' } } }`. Keep `call` ≤ 20 lines; extract sub-services if longer. Validate inputs at top of `call`; return error hash if invalid. Return serialized data only — no raw persistence model objects (e.g. ActiveRecord, ROM) in `response`.285. **Handle Errors and Logging:** Catch `StandardError` (and domain exceptions). Log with the application logger (e.g., `logger.error`). Use `UPPER_SNAKE_CASE` constants for all user-facing error strings — never inline in a `rescue`. Never re-raise to caller.296. **Add YARD Documentation:** Add `@param`, `@return [Hash]`, and `@raise` tags to `self.call` and every other public method. Document `self.call` separately from `#call`. For class-only services (Pattern 3), if the class returns a non-standard shape (e.g. `nil` / error string), document that explicitly in YARD and the README.307. **Write Module README:** Generate `services/<module_name>/README.md` explaining domain context. Required even for single-service modules.3132### Additional Constraints3334| Aspect | Rule |35|--------|------|36| Transactions | Only wrap multi-step database operations that must be atomic |37| Scope | Return data only (no HTTP/UI concerns); single responsibility per service |38| SQL | Use query sanitization for any dynamic queries |39| Shared logic | Extract validators to class-only services (Pattern 3) |4041## Core Patterns4243### 1. The `.call` Pattern44```ruby45def self.call(params)46 new(params).call47end4849def call50 # ... processing ...51 { success: true, response: { data: result } }52rescue StandardError => e53 logger.error("Processing Error: #{e.message}")54 logger.error(e.backtrace.join("\n"))55 { success: false, response: { error: { message: ERROR_MESSAGE } } }56end57```5859### 2. Batch Processing + Per-Item Rescue (Partial Success)60```ruby61def call62 results = @items.each_with_object({ successful: [], failed: [] }) do |item, acc|63 # process...64 rescue StandardError => e65 logger.error("Unexpected item error: #{e.message}")66 acc[:failed] << { sku: item[:sku], error: e.message }67 end68 { success: true, response: results }69end70```7172### 3. Class-only Services (Static Methods)73When no instance state is needed, use ONLY class methods — no `initialize`, no instance variables. Suitable for validators, formatters, and argument-only helpers.7475```ruby76class Orders::QuantityValidator77 def self.call(quantity:)78 return { success: false, response: { error: { message: INVALID_QUANTITY } } } unless quantity.positive?7980 { success: true, response: { valid: true } }81 end82end83```8485### 4. Orchestrator Delegation (≤20-line `call`)86```ruby87def call88 user_result = UserCreationService.call(@params)89 return user_result unless user_result[:success]90 # ... continue ...91end92```9394## Extended Resources (Progressive Disclosure)9596Load these files only when their specific content is needed:9798- **[assets/examples.md](assets/examples.md)** — Detailed examples of the 4 core patterns (Standard, Batch, Static, Orchestrator).99- **[assets/service_skeleton.md](assets/service_skeleton.md)** — Basic starting skeleton.100- **[assets/module_readme_template.md](./assets/module_readme_template.md)** — Template for the mandatory module README.101102## Integration103104| Skill | When to chain |105|-------|---------------|106| **write-yard-docs** | Writing/reviewing inline docs |107| **integrate-api-client** | External API integrations |108| **implement-calculator-pattern** | Variant-based calculators |109| **write-tests** | General testing structure |110| **refactor-process** | Refactoring service objects |