Architecture
Servus wraps service execution with automatic validation, logging, and error handling. When you call Service.call(**args), the framework orchestrates these concerns transparently.
Execution Flow
Arguments
The framework intercepts the .call class method to inject cross-cutting concerns before and after your business logic runs. Your call instance method contains only business logic - validation, logging, event emission, and timing happen automatically.
Core Components
Servus::Base (lib/servus/base.rb): Foundation class providing .call() orchestration and response helpers (success, failure, error!)
Support::Response (lib/servus/support/response.rb): Immutable result object with success?, data, and error attributes
Support::Validator (lib/servus/support/validator.rb): JSON Schema validation for arguments (before execution) and results (after execution). Schemas are cached after first load.
Support::Logger (lib/servus/support/logger.rb): Automatic logging at DEBUG (calls with args), INFO (success), WARN (failures), ERROR (exceptions)
Support::Rescuer (lib/servus/support/rescuer.rb): Declarative exception handling via rescue_from class method
Support::Errors (lib/servus/support/errors.rb): HTTP-aligned error hierarchy (ServiceError, NotFoundError, ValidationError, etc.)
Events::Emitter (lib/servus/events/emitter.rb): DSL for declaring events that services emit on success/failure
Events::Bus (lib/servus/events/bus.rb): Central event router using ActiveSupport::Notifications for thread-safe dispatch
EventHandler (lib/servus/event_handler.rb): Base class for handlers that subscribe to events and invoke services
Extension Points
Schema Validation
Use the schema DSL method to define JSON Schema validation for arguments and results:
class Service < Servus::Base
schema(
arguments: { type: "object", required: ["user_id"] },
result: { type: "object", required: ["user"] }
)
end
Declarative Error Handling
Use rescue_from to convert exceptions into failures. Provide a custom error type or use a block for custom handling.
class Service < Servus::Base
# Default error type
rescue_from Net::HTTPError, Timeout::Error, use: ServiceUnavailableError
# Custom handling with block
rescue_from ActiveRecord::RecordInvalid do |exception|
failure("Validation failed: #{exception.message}", type: ValidationError)
end
end
Support Classes
Create helper classes in app/services/service_name/support/*.rb. These are namespaced to your service.
app/services/process_payment/
Async Execution
Service.call_async(**args) enqueues execution via ActiveJob. The service runs identically whether called sync or async.
ProcessPayment::Service.call_async(
user_id: 1,
amount: 50,
queue: :critical,
wait: 5.minutes
)
Event-Driven Architecture
Services can emit events that trigger downstream handlers. This decouples services from their side effects.
# Service emits events
class CreateUser::Service < Servus::Base
emits :user_created, on: :success
end
# Handler reacts to events
class UserCreatedHandler < Servus::EventHandler
handles :user_created
invoke SendWelcomeEmail::Service, async: true do |payload|
{ user_id: payload[:user_id] }
end
end
See Event Bus for full documentation.
Performance
- Schema loading: Cached per class after first use
- Validation overhead: ~1-5ms when schemas defined
- Logging overhead: ~0.1ms per call
- Total framework overhead: < 10ms per service call