Best Practices
This guide outlines recommended patterns and practices for building robust, maintainable services with async-service.
Application Structure
If you are creating an application that runs services, you should define a top level services.rb file that includes all your service configurations. This file serves as the main entry point for your services. If you are specifically working with the Falcon web server, this file is typically called falcon.rb for historical reasons.
Service Configuration
Create a single top-level service.rb file as your main entry point:
#!/usr/bin/env async-service
# Load your service configurations
require_relative 'lib/my_library/environment/web_environment'
require_relative 'lib/my_library/environment/worker_environment'
service "web" do
	include MyLibrary::Environment::WebEnvironment
end
service "worker" do 
	include MyLibrary::Environment::WorkerEnvironment
end
Multiple Service Configurations
In some cases, you may want to define multiple service configurations, e.g. for different environments or deployment targets. In those cases, you may create web_service.rb or job_service.rb, but this usage should be discouraged.
Library Structure
If you are creating a library that exposes services, use the following structure and guidelines:
Directory Structure
Organize your code following these conventions:
├── service.rb
└── lib/
    └── my_library/
        ├── environment/
        │   ├── web_environment.rb
        │   ├── worker_environment.rb
        │   ├── database_environment.rb
        │   └── tls_environment.rb
        └── service/
            ├── web_service.rb
            └── worker_service.rb
Environment Organization
Place environments in lib/my_library/environment/:
# lib/my_library/environment/web_environment.rb
module MyLibrary
	module Environment
		module WebEnvironment
			include Async::Service::ContainerEnvironment
			
			def service_class
				MyLibrary::Service::WebService
			end
			
			def port
				3000
			end
			
			def host
				"localhost"
			end
		end
	end
end
Service Organization
Place services in lib/my_library/service/:
# lib/my_library/service/web_service.rb
module MyLibrary
	module Service
		class WebService < Async::Service::ContainerService
			private def format_title(evaluator, server)
				if server&.respond_to?(:connection_count)
					"#{self.name} [#{evaluator.host}:#{evaluator.port}] (#{server.connection_count} connections)"
				else
					"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
				end
			end
			
			def run(instance, evaluator)
				# Start your service and return the server object.
				# ContainerService handles container setup, health checking, and process titles.
				start_web_server(evaluator.host, evaluator.port)
			end
			
			private
			
			def start_web_server(host, port)
				# The return value of this method will be the server object which is returned from `run` and passed to `format_title`.
			end
		end
	end
end
Use ContainerEnvironment for Services
Include module Async::Service::ContainerEnvironment for services that run in containers using class Async::Service::ContainerService:
module WebEnvironment
	include Async::Service::ContainerEnvironment
	
	def service_class
		WebService
	end
end
Environment Best Practices
Use Plain Modules
Prefer plain Ruby modules for environments:
module DatabaseEnvironment
	def database_url
		"postgresql://localhost/app"
	end
	
	def max_connections
		10
	end
end
One-to-One Service-Environment Correspondence
Maintain a 1:1 relationship between services and their primary environments:
# Primary environment for WebService
module WebEnvironment
	def service_class
		WebService
	end
	
	# Default configuration:
	def port
		3000
	end
	def host
		'0.0.0.0'
	end
end
# Primary environment for WorkerService  
module WorkerEnvironment
	def service_class
		WorkerService
	end
	
	def queue_name
		'default'
	end
	
	def count
		4
	end
end
Compose with Auxiliary Environments
Use additional environments for cross-cutting concerns:
module WebEnvironment
	include DatabaseEnvironment
	include TLSEnvironment
	include LoggingEnvironment
	
	def service_class
		WebService
	end
end
Service Best Practices
Use ContainerService as Base Class
Prefer Async::Service::ContainerService over Generic for most services:
class WebService < Async::Service::ContainerService
	# ContainerService automatically handles:
	# - Container setup with proper options.
	# - Health checking with process title updates.
	# - Integration with Formatting module.
	
	private def format_title(evaluator, server)
		# Customize process title display
		"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
	end
	
	def run(instance, evaluator)
		# Focus only on your service logic
		start_web_server(evaluator.host, evaluator.port)
	end
end
Implement Meaningful Process Titles
Use the format_title method to provide dynamic process information:
private def format_title(evaluator, server)
	# Good - Include service-specific info
	"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
	
	# Better - Include dynamic runtime status
	if connection_count = server&.connection_count
		"#{self.name} [#{evaluator.host}:#{evaluator.port}] (C=#{format_count connection_count})"
	else
		"#{self.name} [#{evaluator.host}:#{evaluator.port}]"
	end
end
Try to keep process titles short and focused.
Testing Best Practices
Test Environments in Isolation
Test environment modules independently:
# test/my_library/environment/web_environment.rb
describe MyLibrary::Environment::WebEnvironment do
	let(:environment) do
		Async::Service::Environment.build do
			include MyLibrary::Environment::WebEnvironment
		end
	end
	
	it "provides default port" do
		expect(environment.port).to be == 3000
	end
end
Test Services with Service Controller
Use test environments for service testing:
# test/my_library/service/web_service.rb
describe MyLibrary::Service::WebService do
	let(:environment) do
		Async::Service::Environment.build do
			include MyLibrary::Environment::WebEnvironment
		end
	end
	
	let(:evaluator) {environment.evaluator}
	let(:service) {evaluator.service_class(environment, evaluator)}
	let(:controller) {Async::Service::Controller.for(service)}
	
	before do
		controller.start
	end
	after do
		controller.stop
	end
	
	let(:uri) {URI "http://#{evaluator.host}:#{evaluator.port}"}
	
	it "responds to requests" do
		Net::HTTP.get(uri).tap do |response|
			expect(response).to be_a(Net::HTTPSuccess)
		end
	end
end
Note that full end-to-end service tests like this are typically slow and hard to isolate, so it's better to use unit tests for individual components whenever possible.