# API Integration Standards for Crystal
This document outlines the coding standards specifically for API integration in Crystal. It provides guidelines for connecting with backend services and external APIs, ensuring maintainability, performance, and security. These standards leverage the latest features of Crystal.
## 1. Architectural Considerations for API Integration
### 1.1 Service Layer Abstraction
**Standard:** Decouple API interaction logic from application core functionalities using a service layer.
**Do This:** Create a dedicated service layer or module to handle all API-related concerns.
**Don't Do This:** Directly embed API calls within controllers, models, or other unrelated components.
**Why:** This separation of concerns enhances code reusability, testability, and maintainability. It allows for easier changes to API endpoints, request/response formats, or authentication mechanisms without affecting the core application logic.
**Code Example:**
"""crystal
# src/services/user_service.cr
require "http/client"
require "json"
module Services
class UserService
API_URL = "https://api.example.com/users"
def self.fetch_user(user_id : Int32) : Hash(String, String)?
client = HTTP::Client.new(API_URL + "/#{user_id}")
response = client.get
if response.status_code == 200
JSON.parse(response.body).as(Hash(String, String))
else
nil
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
nil
end
end
end
# src/controllers/users_controller.cr
require "./services/user_service"
class UsersController
def show(context : HTTP::Server::Context)
user_id = context.params.dig("id").to_i
user = Services::UserService.fetch_user(user_id)
if user
context.response.content_type = "application/json"
context.response.print user.to_json
else
context.response.status_code = 404
context.response.print "User not found"
end
end
end
"""
### 1.2 Configuration Management
**Standard:** Externalize API configuration details (URLs, API keys, timeouts) using environment variables or configuration files.
**Do This:** Load API configurations from "ENV" or a configuration file.
**Don't Do This:** Hardcode API URLs or credentials directly into source code.
**Why:** Facilitates deployment across different environments (development, staging, production) without requiring code modifications.
**Code Example:**
"""crystal
# config/application.cr
require "yaml"
module AppConfig
struct Config
property api_url : String
property api_key : String
property timeout : Int32
end
@@config : Config? = nil
def self.config : Config
@@config ||= load_config
end
private def self.load_config : Config
config_file_path = "config/config.yml"
config_data = YAML.parse_file(config_file_path).as_h
api_url = ENV["API_URL"] || config_data["api_url"].as(String)
api_key = ENV["API_KEY"] || config_data["api_key"].as(String)
timeout = (ENV["TIMEOUT"] || config_data["timeout"]).as(String).to_i
Config.new(api_url, api_key, timeout)
rescue YAML::ParseException => e
raise "Failed to parse config file: #{e}"
rescue e
raise "Error loading configuration: #{e}"
end
end
# config/config.yml
development:
api_url: "https://dev.api.example.com"
api_key: "dev_api_key"
timeout: 10
production:
api_url: "https://api.example.com"
api_key: "prod_api_key"
timeout: 30
# src/services/user_service.cr
require "./config/application"
module Services
class UserService
def self.fetch_user(user_id : Int32) : Hash(String, String)?
config = AppConfig.config
client = HTTP::Client.new(config.api_url + "/users/#{user_id}")
client.options.timeout = config.timeout * 1_000_000 # Timeout in milliseconds
client.options.headers = {"X-API-Key" => config.api_key}
response = client.get
if response.status_code == 200
JSON.parse(response.body).as(Hash(String, String))
else
nil
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
nil
end
end
end
"""
### 1.3 API Versioning Strategy
**Standard:** Implement a consistent API versioning strategy (URI path, headers, or content negotiation).
**Do This:** Use a versioning scheme that clearly defines the API version being used. Consider URI path versioning "/api/v1/users", header versioning "Accept: application/vnd.example.v1+json" or content negotiation.
**Don't Do This:** Rely on implicit versioning or gradual changes without clear version identifiers.
**Why:** Ensures backward compatibility as the API evolves, allowing clients to choose the version they support.
**Code Example (URI path versioning):**
"""crystal
# src/services/user_service.cr
require "./config/application"
module Services
class UserService
API_VERSION = "v1"
def self.fetch_user(user_id : Int32) : Hash(String, String)?
config = AppConfig.config
client = HTTP::Client.new(config.api_url + "/api/#{API_VERSION}/users/#{user_id}")
client.options.timeout = config.timeout * 1_000_000 # Timeout in milliseconds
client.options.headers = {"X-API-Key" => config.api_key}
response = client.get
if response.status_code == 200
JSON.parse(response.body).as(Hash(String, String))
else
nil
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
nil
end
end
end
# config/routes.cr
require "kemal"
require "./src/controllers/users_controller"
get "/api/v1/users/:id", UsersController.new.show
"""
## 2. Implementation Best Practices
### 2.1 HTTP Client Management
**Standard:** Utilize "HTTP::Client" with proper connection pooling and timeout settings. Avoid creating new clients for each request.
**Do This:** Create a single, reusable "HTTP::Client" instance, configure timeouts appropriately, and handle connection errors gracefully. Consider using a connection pool for increased efficiency.
**Don't Do This:** Create a new "HTTP::Client" instance for every API call. Ignore timeout settings which can lead to hanging requests.
**Why:** Improves performance by reusing connections and protects against potential resource exhaustion. Proper timeout settings prevent indefinite waiting for unresponsive APIs.
**Code Example:**
"""crystal
# src/services/http_service.cr
require "http/client"
require "./config/application"
module Services
class HTTPService
@@client : HTTP::Client? = nil
def self.client : HTTP::Client
@@client ||= create_client
end
private def self.create_client : HTTP::Client
config = AppConfig.config
client = HTTP::Client.new
client.options.timeout = config.timeout * 1_000_000 # Timeout in milliseconds
client
end
def self.get(url : String, headers : Hash(String, String)? = nil) : HTTP::Response
request = HTTP::Request.new("GET", url, HTTP::Headers.new(headers), IO::Memory.new)
client.execute(request)
end
def self.post(url : String, body : String, headers : Hash(String, String)? = nil) : HTTP::Response
request = HTTP::Request.new("POST", url, HTTP::Headers.new(headers || {"Content-Type" => "application/json"}), IO::Memory.new(body))
client.execute(request)
end
end
end
# src/services/user_service.cr
require "./services/http_service"
module Services
class UserService
API_URL = AppConfig.config.api_url
def self.create_user(user_data : Hash(String,String)) : HTTP::Response
url = API_URL + "/users"
body = user_data.to_json
headers = {"X-API-Key" => AppConfig.config.api_key}
Services::HTTPService.post(url, body, headers)
end
end
end
"""
### 2.2 Data Serialization and Deserialization
**Standard:** Use "JSON.parse" and "JSON.stringify" (or other appropriate formats like "YAML") for data serialization/deserialization. Define explicit types for parsed data.
**Do This:** Leverage Crystal's strong typing when parsing API responses. Use "JSON.mapping" in structs for automatic serialization and deserialization.
**Don't Do This:** Use generic "Hash" types without specifying key and value types. Perform manual string manipulation for parsing.
**Why:** Ensures type safety and avoids runtime errors when accessing API data. Serialization/deserialization libraries provide optimized performance compared to manual string manipulation. "JSON.mapping" simplifies handling complex API requests and responses.
**Code Example:**
"""crystal
# src/models/user.cr
require "json"
struct User
JSON.mapping({
id: {type: Int32, key: "id"},
name: {type: String, key: "name"},
email: {type: String, key: "email"}
})
property id : Int32
property name : String
property email : String
end
# src/services/user_service.cr
require "./models/user"
module Services
class UserService
def self.fetch_user(user_id : Int32) : User?
client = HTTP::Client.new("https://api.example.com/users/#{user_id}")
response = client.get
if response.status_code == 200
User.from_json(response.body)
else
nil
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
nil
end
end
end
"""
### 2.3 Error Handling
**Standard:** Implement robust error handling for API calls, including network errors, HTTP status codes (4xx, 5xx), and data parsing exceptions.
**Do This:** Use "begin...rescue" blocks to catch exceptions. Log errors with sufficient context (URL, request parameters, response body). Implement retry mechanisms for transient errors. Raise custom exceptions to signal specific API errors.
**Don't Do This:** Ignore exceptions or simply print error messages without handling them. Assume all API calls succeed.
**Why:** Prevents application crashes and provides valuable debugging information. Retry mechanisms improve resilience against temporary API outages. Custom exceptions allow for more granular error handling.
**Code Example:**
"""crystal
module Services
class APIError < Exception
def initialize(message : String, status_code : Int32?)
super(message)
@status_code = status_code
end
property status_code : Int32?
end
class UserService
def self.fetch_user(user_id : Int32) : User?
begin
client = HTTP::Client.new("https://api.example.com/users/#{user_id}")
response = client.get
case response.status_code
when 200
User.from_json(response.body)
when 404
raise APIError.new("User not found", 404)
else
raise APIError.new("Unexpected API error: #{response.status_code}", response.status_code)
end
rescue e : HTTP::Error => ex
puts "Network error: #{ex}"
nil # Or retry logic
rescue e : JSON::ParseException => ex
puts "Failed to parse JSON: #{ex}"
nil
rescue e : APIError => ex
puts "API Error: #{ex.message}, Status Code: #{ex.status_code}"
nil
end
end
end
end
#Example usage in controller/handler
user = Services::UserService.fetch_user(123)
if user
puts "User found: #{user.name}"
else
puts "User not found or error occurred."
end
"""
### 2.4 Authentication and Authorization
**Standard:** Implement secure authentication and authorization mechanisms based on the API's requirements (API keys, OAuth 2.0, JWT).
**Do This:** Store API keys securely and avoid committing them directly to version control. Use environment variables or encrypted configuration files. Use established libraries for OAuth 2.0 or JWT handling. Follow the principle of least privilege when granting API access.
**Don't Do This:** Hardcode API keys in the application code. Use weak or outdated authentication methods. Grant excessive permissions that are not required.
**Code Example (API Key via Header):**
"""crystal
# src/services/user_service.cr
require "./config/application"
module Services
class UserService
def self.fetch_user(user_id : Int32) : Hash(String, String)?
config = AppConfig.config
client = HTTP::Client.new("https://api.example.com/users/#{user_id}")
client.options.headers = {"X-API-Key" => config.api_key}
response = client.get
if response.status_code == 200
JSON.parse(response.body).as(Hash(String, String))
else
nil
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
nil
end
end
end
"""
**Code Example (OAuth 2.0 using a library like "oauth2"):**
"""crystal
#Requires shards install oauth2
require "oauth2"
module Services
class OAuthService
CLIENT_ID = ENV["OAUTH_CLIENT_ID"]
CLIENT_SECRET = ENV["OAUTH_CLIENT_SECRET"]
AUTHORIZE_URL = "https://example.com/oauth/authorize"
TOKEN_URL = "https://example.com/oauth/token"
REDIRECT_URI = "http://localhost:3000/callback"
@@client : OAuth2::Client? = nil
def self.client : OAuth2::Client
@@client ||= OAuth2::Client.new(CLIENT_ID, CLIENT_SECRET,
authorize_url: AUTHORIZE_URL,
token_url: TOKEN_URL)
end
def self.get_authorization_url
Self.client.authorize_url(redirect_uri: REDIRECT_URI)
end
def self.get_token(code : String) : OAuth2::AccessToken
Self.client.access_token_from_authorization_code(code, redirect_uri: REDIRECT_URI)
end
end
}
"""
### 2.5 Rate Limiting
**Standard:** Implement client-side rate-limiting strategies to avoid exceeding API usage limits.
**Do This:** Respect the API's rate-limiting headers (e.g., "X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"). Use a rate-limiting library like "ratelimit.cr" or implement a custom solution using a token bucket or leaky bucket algorithm.
**Don't Do This:** Ignore rate limits, which can lead to API blocking or account suspension. Make excessive API calls without considering the API's limitations.
**Code Example:**
"""crystal
require "http/client"
require "json"
require "time"
module Services
class UserService
@@last_request_time : Time? = nil
@@min_delay : Float64 = 0.2 # Minimum delay in seconds between requests
def self.fetch_user(user_id : Int32)
delay_request
client = HTTP::Client.new("https://api.example.com/users/#{user_id}")
response = client.get
if response.status_code == 200
JSON.parse(response.body).as(Hash(String,String))
else
puts "Error: #{response.status_code}"
nil
end
end
private def self.delay_request
if @@last_request_time
elapsed = Time.utc - @@last_request_time
if elapsed < @@min_delay
sleep(@@min_delay-elapsed)
end
end
@@last_request_time = Time.utc
end
end
end
"""
## 3. Asynchronous API Calls
### 3.1 Concurrency with Fibers
**Standard:** Utilize Crystal's built-in Fibers for non-blocking API calls
**Do This:** Wrap API calls which may be slow inside of Fibers so that your application can continue to do other things in the meantime. Use "Channel" to pass results from a fiber back to the parent process.
**Don't Do This:** Perform blocking API calls on the main thread.
**Why:** This will improve the performance and responsiveness of the application.
**Code Example:**
"""crystal
require "http/client"
require "json"
module Services
class UserService
def self.fetch_user_async(user_id : Int32)
channel = Channel(Hash(String, String)?).new
spawn do
begin
client = HTTP::Client.new("https://api.example.com/users/#{user_id}")
response = client.get
if response.status_code == 200
channel.send(JSON.parse(response.body).as(Hash(String, String)))
else
channel.send(nil)
end
rescue e : HTTP::Error
puts "Error fetching user: #{e}"
channel.send(nil)
end
end
channel.receive
end
end
end
#Usage example
user_data = Services::UserService.fetch_user_async(123)
if user_data
puts "User Data: #{user_data}"
else
puts "Failed to fetch user data."
end
"""
## 4. Testing API Integrations
### 4.1 Mocking and Stubbing
**Standard:** Mock API calls in unit tests to isolate components and avoid external dependencies.
**Do This:** Use a mocking library like "spec_helper!" or create manual mocks using inherited classes, to replace real API calls with controlled responses. Assert that API calls are made with the correct parameters.
**Don't Do This:** Perform real API calls during unit tests. Neglect to test error handling scenarios.
**Why:** Ensures that tests are fast, reliable, and predictable. Allows for testing different API responses (success, error, timeouts) without relying on the actual API's availability.
**Code Example:**
"""crystal
# spec/services/user_service_spec.cr
require "spec"
require "./src/services/user_service"
describe Services::UserService do
describe ".fetch_user" do
it "fetches a user successfully" do
HTTP::Client.should_receive(:get).with("https://api.example.com/users/1").and_return(
HTTP::Response.new(200, {}, IO::Memory.new('{"id": 1, "name": "John Doe", "email": "john.doe@example.com"}'))
)
user = Services::UserService.fetch_user(1)
user.should be_a(Hash)
user?["name"].should eq("John Doe")
end
it "returns nil when the API returns a 404" do
HTTP::Client.should_receive(:get).with("https://api.example.com/users/1").and_return(
HTTP::Response.new(404, {}, IO::Memory.new(""))
)
user = Services::UserService.fetch_user(1)
user.should be_nil
end
end
end
"""
### 4.2 Integration Tests
**Standard:** Implement integration tests to verify the interaction between different components, including API calls.
**Do This:** Use a separate testing environment with a stable API endpoint or a mock API server. Verify end-to-end functionality, including data serialization/deserialization, error handling, and authentication. Test with real credentials but using test accounts.
**Don't Do This:** Skip integration tests or rely solely on unit tests. Use production API credentials during testing.
**Why:** Ensures that the application works correctly with the API under realistic conditions. Detects integration issues that might not be caught by unit tests.
## 5. Documentation
### 5.1 API Usage
**Standard:** Document all API interactions within the codebase.
**Do This:** Document the service used when calling external API. The expected schemas for requests and responses. The type of authentication necessary. The errors and respective handling strategy.
**Don't Do This:** Assume developers understand the integration and respective nuances on their own.
**Why:** To reduce friction within the team collaborating on a project which makes API calls.
This document provides a comprehensive set of guidelines for API integration in Crystal, promoting maintainable, performant, and secure code. Adhering to these standards will improve the overall quality and robustness of Crystal applications interacting with external APIs.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Component Design Standards for Crystal This document outlines component design standards for Crystal projects, aiming to foster reusable, maintainable, and performant code. These standards are tailored specifically for Crystal's features and ecosystem. ## 1. Component Architecture A component is a self-contained, reusable unit of functionality with a well-defined interface. In Crystal, components can be represented by modules, classes, structs, or even standalone functions. The overall application architecture should be component-based to promote modularity. ### 1.1. Loose Coupling * **Do This:** Design components with minimal knowledge of each other. Use interfaces or abstract classes to communicate. * **Don't Do This:** Create tight dependencies between components, making them difficult to change or reuse independently. * **Why:** Loose coupling enhances modularity and flexibility. Changes in one component are less likely to affect others. """crystal # Good: Using an interface abstract class PaymentProcessor abstract def process_payment(amount : Float64) end class StripeProcessor < PaymentProcessor def process_payment(amount : Float64) puts "Processing #{amount} with Stripe" end end class PaypalProcessor < PaymentProcessor def process_payment(amount : Float64) puts "Processing #{amount} with Paypal" end end # The client depends on the abstract PaymentProcessor, not the concrete implementations class Order def initialize(@payment_processor : PaymentProcessor) end def checkout(amount : Float64) @payment_processor.process_payment(amount) end end stripe = StripeProcessor.new order = Order.new(stripe) order.checkout(100.0) """ """crystal # Bad: Tightly Coupled class StripePayment def process(amount : Float64) puts "Processing payment using Stripe: #{amount}" end end class OrderProcessor def process_order(amount : Float64) stripe = StripePayment.new stripe.process(amount) # Tight coupling - OrderProcessor knows too much about StripePayment end end order_processor = OrderProcessor.new order_processor.process_order(50.0) """ ### 1.2. High Cohesion * **Do This:** Ensure each component encapsulates a single, well-defined purpose. * **Don't Do This:** Create God classes or modules that try to handle too many unrelated responsibilities. * **Why:** High cohesion leads to simpler, more understandable code. It simplifies maintenance and testing. """crystal # Good: High Cohesion module UserAuthentication def self.authenticate(username : String, password : String) : Bool # ... authentication logic ... true # Stubbed for example end def self.authorize(user_id : Int32, resource : String) : Bool # ... authorization logic ... true # Stubbed for example end end # The module is focused solely on user authentication and authorization """ """crystal # Bad: Low Cohesion module UserManagement def self.authenticate(username : String, password : String) : Bool # ... authentication logic ... true # Stubbed for example end def self.create_user(username : String, email : String) # ... user creation logic ... end def self.send_welcome_email(email : String) # ... send welcome email logic ... end end # The module attempts to handle authentication, user creation, and email sending - low cohesion! """ ### 1.3. Explicit Interfaces * **Do This:** Define clear interfaces for each component using abstract classes or modules with defined public methods. * **Don't Do This:** Rely on implicit interfaces or expose internal implementation details. * **Why:** Explicit interfaces promote predictability and allow for easier substitution of components. """crystal # Good: Interface definition using abstract class abstract class Storage abstract def save(key : String, value : String) abstract def load(key : String) : String? # String? to indicate nullable return abstract def delete(key : String) end class FileStorage < Storage def save(key : String, value : String) File.write(key, value) end def load(key : String) : String? File.read(key) rescue nil end def delete(key : String) File.delete(key) end end # Usage: storage = FileStorage.new storage.save("my_key", "my_value") puts storage.load("my_key") """ """crystal # Bad: Implicit Interface class DataWriter def write_data(data) # Implementation implies it takes any data puts "Writing data: #{data}" end end class DataProcessor def process(writer) writer.write_data({"key" => "value"}) # Assumes DataWriter has write_data, but no explicit contract. end end """ ## 2. Component Implementation ### 2.1. Use Crystal Generics Judiciously * **Do This:** Use generics to create reusable components that work with different types. * **Don't Do This:** Overuse generics, leading to complex and unreadable code. Consider type constraints. * **Why:** Generics enhance code reuse while maintaining type safety. """crystal # Good: Using Generics with Type Constraints class Cache(T) getter data : Hash(String, T) def initialize @data = Hash(String, T).new end def put(key : String, value : T) @data[key] = value end def get(key : String) : T? @data[key] end end cache_string = Cache(String).new cache_string.put("name", "Crystal") puts cache_string.get("name") cache_int = Cache(Int32).new cache_int.put("port", 8080) puts cache_int.get("port") """ """crystal # Bad: Overusing Generics class GenericProcessor(T, U, V) # Too many generic types, hard to reason about def process(input1 : T, input2 : U) : V # Complex logic involving T, U, and V... input1.to_s + input2.to_s # Just an example end end """ ### 2.2. Favor Composition over Inheritance * **Do This:** Build complex components by combining simpler components rather than relying heavily on inheritance. * **Don't Do This:** Create deep inheritance hierarchies that become rigid and difficult to maintain. * **Why:** Composition provides better flexibility and avoids the problems associated with the fragile base class problem. """crystal # Good: Composition example class Logger def log(message : String) puts "Log: #{message}" end end class StatsCollector def collect_stats(data) puts "Collecting Stats: #{data}" end end class Service def initialize(@logger : Logger, @stats : StatsCollector) end def run(data) @logger.log("Starting service") @stats.collect_stats(data) puts "Service running with #{data}" @logger.log("Service completed") end end logger = Logger.new stats = StatsCollector.new service = Service.new(logger, stats) service.run({"requests" => 100}) """ """crystal # Bad: Deep Inheritance class BaseComponent def initialize puts "Base Initialized" end end class ExtendedComponent < BaseComponent def initialize super # Always remember to call super! puts "Extended initialized" end end class EvenMoreExtended < ExtendedComponent def initialize super # Remember to call super! puts "EvenMoreExtended Initialized" end end component = EvenMoreExtended.new # Difficult to trace initialization flow. Inheritance makes debugging harder. """ ### 2.3. Use Modules for Namespaces and Utility Functions * **Do This:** Group related functions and constants within modules to avoid naming conflicts and organize code. * **Don't Do This:** Define global functions and constants that pollute the namespace. Modules offer a better approach. * **Why:** Modules create clear boundaries and improve code organization. """crystal # Good: Module Usage module StringUtils def self.reverse(str : String) : String str.reverse end def self.capitalize(str : String) : String str.capitalize end end puts StringUtils.reverse("hello") # olleh puts StringUtils.capitalize("world") # World """ """crystal # Bad: Global functions def reverse_string(str : String) : String str.reverse end def capitalize_string(str : String) : String str.capitalize end puts reverse_string("hello") puts capitalize_string("world") # pollute the global namespace """ ### 2.4. Immutability Where Possible * **Do This:** Favor immutable data structures and components to avoid unexpected side effects. * **Don't Do This:** Mutate data structures directly whenever possible. * **Why:** Immutability simplifies reasoning about code and improves thread safety. """crystal # Good: Immutability record Point, x : Int32, y : Int32 point1 = Point.new(1, 2) point2 = Point.new(point1.x + 1, point1.y + 1) # Creating new instance, not mutating puts point1 puts point2 """ """crystal # Bad: Mutable State class MutableCounter property count : Int32 def initialize @count = 0 end def increment @count += 1 # Mutates the state directly end end counter = MutableCounter.new counter.increment puts counter.count """ ### 2.5. Error Handling Strategy * **Do This:** Employ exceptions and "Result" types to deal with errors gracefully. * **Don't Do This:** Ignore errors or rely on return codes without proper handling. * **Why:** Robust error handling prevents application crashes and allows for graceful recovery. """crystal # Good: Using "Result" type for error handling require "option_parser" def safe_divide(a : Float64, b : Float64) : Result(Float64, String) if b == 0.0 return Error("Cannot divide by zero") else return Ok(a / b) end end result = safe_divide(10.0, 2.0) if result.success? puts "Result: #{result.value}" else puts "Error: #{result.error}" end result = safe_divide(5.0, 0.0) if result.success? puts "Result: #{result.value}" else puts "Error: #{result.error}" end """ """crystal # Bad: Ignoring Errors def divide(a : Float64, b : Float64) : Float64 a / b # No handling of potential division by zero end puts divide(10.0, 0.0) # Will crash the program """ ## 3. Specific Design Patterns for Crystal ### 3.1. Strategy Pattern * **How:** Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it. """crystal # Strategy Interface abstract class CompressionStrategy abstract def compress(file_path : String) end # Concrete Strategies class ZipCompression < CompressionStrategy def compress(file_path : String) puts "Compressing #{file_path} using ZIP" end end class GzipCompression < CompressionStrategy def compress(file_path : String) puts "Compressing #{file_path} using GZIP" end end # Context class FileCompressor def initialize(@strategy : CompressionStrategy) end def compress_file(file_path : String) @strategy.compress(file_path) end end # Client Code zip_strategy = ZipCompression.new gzip_strategy = GzipCompression.new compressor = FileCompressor.new(zip_strategy) compressor.compress_file("my_file.txt") compressor = FileCompressor.new(gzip_strategy) compressor.compress_file("another_file.txt") """ ### 3.2. Observer Pattern * **How:** Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. """crystal # Subject interface class Subject def initialize @observers = [] of Observer end def attach(observer : Observer) @observers << observer end def detach(observer : Observer) @observers.delete(observer) end def notify @observers.each { |observer| observer.update(self) } end end # Concrete Subject class NewsPublisher < Subject getter news : String def news=(news : String) @news = news notify # Notify after state change end end # Observer Interface abstract class Observer abstract def update(subject : Subject) end # Concrete Observers class EmailSubscriber < Observer def update(subject : Subject) puts "Email Subscriber received news: #{subject.news}" end end class SMSSubscriber < Observer def update(subject : Subject) puts "SMS Subscriber received news: #{subject.news}" end end # Client Code publisher = NewsPublisher.new email_subscriber = EmailSubscriber.new sms_subscriber = SMSSubscriber.new publisher.attach(email_subscriber) publisher.attach(sms_subscriber) publisher.news = "Crystal 1.7 released!" """ ### 3.3. Factory Pattern * **How:** Define an interface for creating an object, but let subclasses decide which class to instantiate. Factory Method lets a class defer instantiation to subclasses. """crystal # Abstract Product abstract class Document abstract def open end # Concrete Products class PDFDocument < Document def open puts "Opening PDF document" end end class TextDocument < Document def open puts "Opening Text document" end end # Abstract Factory abstract class DocumentFactory abstract def create_document : Document end # Concrete Factories class PDFFactory < DocumentFactory def create_document : Document PDFDocument.new end end class TextFactory < DocumentFactory def create_document : Document TextDocument.new end end # Client Code pdf_factory = PDFFactory.new pdf_document = pdf_factory.create_document pdf_document.open text_factory = TextFactory.new text_document = text_factory.create_document text_document.open """ ## 4. Code Style and Formatting * **Use "crystal tool format":** Consistently format code using the built-in formatter. * **Follow Naming Conventions:** class "PascalCase", method "snake_case". * **Limit Line Length:** Aim for a maximum of 80-120 characters per line. * **Add Comments:** Clear and concise comments to explain complex logic. Use proper documentation for public APIs. ## 5. Conclusion Adhering to these component design standards will significantly improve the quality, maintainability, and reusability of Crystal codebases. This document will serve as a guide for new developers and a reference for maintaining consistent code quality across projects.
# Code Style and Conventions Standards for Crystal This document outlines the code style and conventions standards for the Crystal programming language. Adhering to these guidelines will promote code consistency, readability, and maintainability, and contribute significantly to collaborative development efforts. These guidelines are designed to align with the latest version of Crystal and leverage best practices in the Crystal ecosystem. The intention of this document is to also act as a contextual document that can be used in tandem with AI coding assistants such as GitHub Copilot. ## 1. General Formatting ### 1.1. Whitespace * **Do This:** Use 2 spaces for indentation. Avoid tabs. * **Don't Do This:** Use tabs or more/
# Core Architecture Standards for Crystal This document outlines the core architectural standards for Crystal projects. It aims to provide clear guidelines for project structure, architectural patterns, and organization principles, specifically tailored for Crystal. Following these standards will promote maintainability, performance, security, and overall code quality. These standards use Crystal version "1.11.2". ## 1. Project Structure and Organization ### 1.1 General Structure * **Do This:** Adhere to a consistent and predictable project structure. A recommended structure is: """ my_project/ ├── src/ # Source code │ ├── my_project/ # Module directory │ │ ├── models/ # Data models │ │ │ ├── user.cr │ │ │ └── product.cr │ │ ├── controllers/ # Request Handlers │ │ │ ├── user_controller.cr │ │ │ └── product_controller.cr │ │ ├── services/ # Business Logic │ │ │ ├── user_service.cr │ │ │ └── product_service.cr │ │ ├── repositories/ # Data Access Layer │ │ │ ├── user_repository.cr │ │ │ └── product_repository.cr │ │ ├── validators/ # Data validation logic │ │ │ ├── user_validator.cr │ │ │ └── product_validator.cr │ │ ├── utils/ # Supporting utilities │ │ │ ├── string_utils.cr │ │ │ └── date_utils.cr │ │ ├── application.cr # Main application logic │ │ └── web.cr # Web framework initialization │ ├── main.cr # Entry point ├── spec/ # Tests │ ├── spec_helper.cr # Test setup │ ├── models/ │ │ ├── user_spec.cr │ │ └── product_spec.cr │ ├── controllers/ │ │ ├── user_controller_spec.cr │ │ └── product_controller_spec.cr ├── shards.yml # Dependency management ├── .gitignore # Git ignore file └── README.md # Project overview """ * **Don't Do This:** Scatter code arbitrarily throughout the project or create excessively deep directory structures. * **Why:** A well-defined project structure enhances navigability, understandability, and maintainability. It promotes separation of concerns and makes it easier for developers to locate specific components. ### 1.2 Module Organization * **Do This:** Group related code into modules that reflect logical components (e.g., "MyProject::Users", "MyProject::Products"). """crystal module MyProject module Users class User # ... end end module Products class Product # ... end end end """ * **Don't Do This:** Place all code in a single, monolithic module, or create modules that don't represent meaningful groupings. * **Why:** Modules provide namespaces and prevent naming conflicts. They also make it easier to manage code dependencies. ### 1.3 Layered Architecture * **Do This:** Implement a layered architecture. Each layer has a specific responsibility. Common layers include: * **Presentation Layer (Controllers):** Handles user interaction and request/response cycles. * **Application Layer (Services):** Orchestrates business logic. * **Domain Layer (Models):** Represents core business entities and logic. * **Infrastructure Layer (Repositories):** Manages data persistence and external integrations. * **Don't Do This:** Allow layers to directly depend on other layers that are not immediately adjacent (e.g., controller directly accessing a database repository). * **Why:** Layered architectures promote separation of concerns and improve testability. Changes in one layer are less likely to impact other layers. """crystal # src/my_project/controllers/user_controller.cr require "./services/user_service" module MyProject module Controllers class UserController def initialize(@user_service : Services::UserService) end def create(params : Hash(String, String)) : HTTP::Response begin user = @user_service.create(params) HTTP::Response.ok(user.to_json) rescue => e HTTP::Response.bad_request(e.message) end end end end end # src/my_project/services/user_service.cr require "./repositories/user_repository" require "./validators/user_validator" module MyProject module Services class UserService def initialize(@user_repository : Repositories::UserRepository, @user_validator : Validators::UserValidator) end def create(params : Hash(String, String)) : Users::User user = Users::User.new(username: params["username"], email: params["email"]) if @user_validator.valid?(user) @user_repository.create(user) else raise "Invalid user data" end end end end end # src/my_project/repositories/user_repository.cr require "./models/user" require "sqlite3" module MyProject module Repositories class UserRepository def initialize(@db : SQLite3::Database = SQLite3::Database.new("my_database.db")) @db.execute "CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, username TEXT, email TEXT)" end def create(user : Users::User) : Users::User statement = @db.prepare "INSERT INTO users (username, email) VALUES (?, ?)" statement.bind(1, user.username) statement.bind(2, user.email) statement.execute user end end end end # src/my_project/models/user.cr require "json" module MyProject module Users class User include JSON::Serializable property id : Int32? property username : String property email : String def initialize(username : String, email : String, id : Int32? = nil) @id = id @username = username @email = email end end end end # src/my_project/validators/user_validator.cr module MyProject module Validators class UserValidator def valid?(user : Users::User) : Bool !user.username.empty? && user.email.includes?("@") end end end end """ ## 2. Design Patterns ### 2.1 Dependency Injection * **Do This:** Use dependency injection through constructor injection and setter injection. """crystal # Constructor injection class MyClass def initialize(@dependency : Dependency) end def do_something @dependency.some_method end end # Setter injection class AnotherClass property dependency : Dependency def set_dependency(dependency : Dependency) @dependency = dependency end def do_something_else @dependency.another_method end end """ * **Don't Do This:** Create tight coupling between classes by directly instantiating dependencies within a class. * **Why:** Dependency injection promotes loose coupling, which enhances testability and flexibility because a mock or stub dependency can easily replace it in a test. Dependencies become explicit, improving code readability. ### 2.2 Factory Pattern * **Do This:** Use a factory pattern to encapsulate the logic of object creation, particularly when the creation process is complex or involves multiple steps. """crystal abstract class PaymentGateway abstract def process_payment(amount : Float) : Bool end class CreditCardGateway < PaymentGateway def process_payment(amount : Float) : Bool puts "Processing credit card payment: #{amount}" true # Simulated successful payment end end class PayPalGateway < PaymentGateway def process_payment(amount : Float) : Bool puts "Processing PayPal payment: #{amount}" true # Simulated successful payment end end class PaymentGatewayFactory def self.create_gateway(type : String) : PaymentGateway case type when "credit_card" CreditCardGateway.new when "paypal" PayPalGateway.new else raise "Invalid payment gateway type: #{type}" end end end # Usage gateway = PaymentGatewayFactory.create_gateway("paypal") gateway.process_payment(100.50) # => Processing PayPal payment: 100.5 """ * **Don't Do This:** Embed object creation logic directly within client classes, especially when dealing with conditional object instantiation. * **Why:** The factory pattern decouples client code from the object creation process, allowing for greater flexibility and maintainability. It centralizes object creation logic, making it easier to modify or extend. ### 2.3 Observer Pattern * **Do This:** Implement the observer pattern when you need to notify multiple dependent objects (observers) about changes to a subject object. """crystal class Subject getter observers : Array(Observer) = [] of Observer def attach(observer : Observer) @observers << observer end def detach(observer : Observer) @observers.delete(observer) end def notify @observers.each { |observer| observer.update(self) } end end abstract class Observer abstract def update(subject : Subject) end class ConcreteObserver < Observer def initialize(@name : String) end def update(subject : Subject) puts "#{@name} received update from subject" end end # Usage subject = Subject.new observer1 = ConcreteObserver.new("Observer 1") observer2 = ConcreteObserver.new("Observer 2") subject.attach(observer1) subject.attach(observer2) subject.notify # => Observer 1 received update from subject # => Observer 2 received update from subject subject.detach(observer1) subject.notify # => Observer 2 received update from subject """ * **Don't Do This:** Create tight dependencies between subject and observer classes where the subject is required to know about the concrete types of the observers. * **Why:** The observer pattern establishes a one-to-many dependency between objects without tightly coupling them. This simplifies and improves flexibility by allowing components to react to changes without the subject needing to be aware of their implementation. ## 3. Concurrency and Parallelism ### 3.1 Fibers for Concurrency * **Do This:** Use "Fiber" for I/O-bound concurrency tasks to prevent blocking the main thread, improving responsiveness. """crystal require "socket" def handle_connection(client : TCPSocket) begin puts "Handling client: #{client.remote_address}" loop do data = client.gets break unless data puts "Received: #{data.strip} from #{client.remote_address}" client.puts "Echo: #{data.strip}" end ensure puts "Closing connection with #{client.remote_address}" client.close end end server = TCPServer.new("127.0.0.1", 3000) puts "Server listening on port 3000" loop do client = server.accept Fiber.new { handle_connection(client) }.resume # start each connection in its own Fiber end """ * **Don't Do This:** Perform long-running I/O operations directly in the main thread, blocking other operations. * **Why:** "Fiber" provides lightweight concurrency, allowing the application to handle multiple tasks concurrently without the overhead of threads. ### 3.2 Parallelism with "spawn" * **Do This:** Utilize "spawn" for CPU-bound tasks to leverage multiple cores and achieve parallelism. Use "Channel" to communicate and synchronize data between spawned processes. """crystal require "concurrent" def process_data(data : Array(Int32)) : Int32 sum = 0 data.each { |x| sum += x } sum end data = (1..1000).to_a.map(&.to_i32) chunk_size = 250 chunks = data.each_slice(chunk_size).to_a channel = Concurrent::Channel.new chunks.each do |chunk| spawn do result = process_data(chunk) channel.send(result) end end total = 0 chunks.size.times do total += channel.receive end puts "Total: #{total}" """ * **Don't Do This:** Overuse "spawn" for simple tasks, which can introduce unnecessary overhead. * **Why:** "spawn" allows executing code in parallel on multiple cores, resulting in faster processing. ### 3.3 Channels for Communication * **Do This:** Employ "Channel" for safe communication and synchronization between fibers or processes. """crystal channel = Channel(String).new spawn do channel.send("Hello from Fiber 1") end spawn do message = channel.receive puts "Fiber 2 received: #{message}" end """ * **Don't Do This:** Share mutable state directly between concurrent entities without proper synchronization mechanisms, leading to race conditions and data corruption. * **Why:** Channels provide a safe and reliable way to exchange data between concurrent entities, preventing data races and ensuring that data is accessed in a synchronized manner. ## 4. Error Handling ### 4.1 Explicit Error Handling * **Do This:** Handle errors explicitly using "begin...rescue...end" blocks to catch and manage exceptions. """crystal begin # Code that may raise an exception result = 10 / 0 puts "Result: #{result}" # This line will not be reached if an exception occurs rescue ex : Exception puts "Error: #{ex.message}" end """ * **Don't Do This:** Ignore potential errors or rely on default exception handling, which can lead to unexpected behavior. * **Why:** Explicit error handling ensures that your application can gracefully recover from errors and provide meaningful feedback to the user or administrator. ### 4.2 Custom Exceptions * **Do This:** Define and use custom exception classes to provide more specific error information. """crystal class AuthenticationError < Exception end def authenticate(username, password) raise AuthenticationError.new("Invalid username or password") unless username == "admin" && password == "secret" true end begin authenticate("user", "wrong_password") rescue ex : AuthenticationError puts "Authentication failed: #{ex.message}" end """ * **Don't Do This:** Use generic "Exception" classes for all errors, limiting the ability to handle specific error types. * **Why:** Custom exceptions allow for more granular error handling and provide better context for debugging. ### 4.3 Logging Errors * **Do This:** Log errors with sufficient detail (timestamp, error message, stack trace) to help diagnose and resolve issues. """crystal require "logger" $log = Logger.new(STDOUT) $log.level = Logger::INFO begin # Code that may raise an exception result = 10 / 0 puts "Result: #{result}" # This line will not be reached if an exception occurs rescue ex : Exception $log.error "Error: #{ex.message}" $log.error ex.backtrace.join("\n") end """ * **Don't Do This:** Omit error logging or log insufficient information, making it difficult to troubleshoot problems. * **Why:** Comprehensive error logging is crucial for identifying and resolving issues in a production environment. ## 5. Data Handling and Persistence ### 5.1 ORM Usage * **Do This:** Use an ORM like "Granite" or roll your own data mapping layer for database interactions to abstract away raw SQL. This enables easier switching of databases and improves readability. """crystal # Example using Granite (assuming setup already done) require "granite" # Model definition class User < Granite::Base column name : String column email : String end # Create a new user user = User.new(name: "John Doe", email: "john@example.com") user.save # Fetch a user retrieved_user = User.find(user.id) puts retrieved_user.name # Output: John Doe """ * **Don't Do This:** Embed raw SQL queries directly within application logic, making maintenance complex and increasing the risk of SQL injection vulnerabilities. * **Why:** ORMs provide a higher level of abstraction for data access, simplifying database operations, preventing SQL injection vulnerabilities, and improving code readability. ### 5.2 Data Validation * **Do This:** Implement data validation to ensure data integrity and prevent invalid data from being persisted. """crystal class User property name : String property email : String def valid? !name.empty? && email.includes?("@") end end user = User.new(name: "", email: "invalid-email") if user.valid? puts "User is valid" else puts "User is invalid" # Output: User is invalid end """ * **Don't Do This:** Trust user input or external data sources without validation and sanitation. * **Why:** Data validation prevents common errors and security vulnerabilities by ensuring that only valid data is processed and stored. ### 5.3 Database Connection Management * **Do This:** Use a connection pool to efficiently manage database connections. This is especially important for web applications to handle concurrent requests. Implement the pool using a library like "dbcp". """crystal require "dbcp" require "sqlite3" pool = DBCP::Pool.new(SQLite3::Database, "mydb.db", max_size: 10) pool.acquire do |db| db.execute "SELECT * FROM users" do |result| puts result.inspect end end """ * **Don't Do This:** Create and close database connections for each operation which creates unneeded overhead. * **Why:** Connection pooling reuses database connections and improves performance. ## 6. Security ### 6.1 Input Sanitization * **Do This:** Sanitize all user inputs to prevent cross-site scripting (XSS) and other injection attacks. """crystal require "cgi" def sanitize(input : String) : String CGI.escape_html(input) end unsanitized_input = "<script>alert('XSS')</script>Hello" sanitized_input = sanitize(unsanitized_input) puts sanitized_input # Output: <script>alert('XSS')</script>Hello """ * **Don't Do This:** Output user-provided data directly without sanitization, potentially exposing your application to security vulnerabilities. * **Why:** Input sanitization transforms malicious input into benign output, preventing XSS and other injection attacks. ### 6.2 Authentication and Authorization * **Do This:** Implement robust authentication and authorization mechanisms to protect sensitive data and functionality. Use established libraries like "Kemal", "Lucky" or devise your own solution with bcrypt for password hashing. """crystal require "bcrypt" def hash_password(password : String) : String BCrypt::Password.create(password) end def verify_password(password : String, hash : String) : Bool BCrypt::Password.new(hash) == password end hashed_password = hash_password("my_secret_password") puts "Hashed password: #{hashed_password}" is_valid = verify_password("my_secret_password", hashed_password) puts "Password is valid: #{is_valid}" # Output: Password is valid: true """ * **Don't Do This:** Use weak or nonexistent authentication and authorization, or hardcode credentials in the application. * **Why:** Robust authentication ensures that only authorized users can access sensitive resources. ### 6.3 HTTPS * **Do This:** Enforce HTTPS to encrypt all communication between the client and server. * **Don't Do This:** Transmit sensitive data over unencrypted HTTP connections. * **Why:** HTTPS protects data in transit from eavesdropping and tampering. ### 6.4 CSRF Protection * **Do This:** Implement CSRF (Cross-Site Request Forgery) protection for all state-changing requests through hidden tokens and Origin/Referer header validation. * **Don't Do This:** Allow all requests without CSRF validation because attackers could create malicious requests on behalf of other users. * **Why:** CSRF protection prevents attackers from forcing authenticated users to perform actions without their knowledge or consent. ## 7. Code Style and Formatting ### 7.1 Naming Conventions * **Do This:** Follow consistent naming conventions. Use "snake_case" for variables, methods, and files. Use "PascalCase" for classes and modules. Constants should use "UPPER_SNAKE_CASE". """crystal module MyModule class MyClass MY_CONSTANT = "constant_value" def my_method(my_variable : Int32) local_variable = my_variable * 2 puts local_variable end end end """ * **Don't Do This:** Use inconsistent or unclear naming conventions, making code harder to understand. * **Why:** Consistent naming improves code readability and maintainability. ### 7.2 Code Comments * **Do This:** Write clear and concise code comments to explain complex logic or non-obvious behavior. Use YARD-style comments for documenting APIs. """crystal # This method calculates the area of a rectangle. # # Args: # width (Float): The width of the rectangle. # height (Float): The height of the rectangle. # # Returns: # Float: The area of the rectangle. def calculate_area(width : Float, height : Float) : Float width * height end """ * **Don't Do This:** Write excessive or redundant comments that state the obvious or are outdated. * **Why:** Meaningful comments enhance code understandability and maintainability. ### 7.3 Line Length * **Do This:** Limit line length to 80-120 characters to improve readability. * **Don't Do This:** Write excessively long lines that require horizontal scrolling. * **Why:** Shorter lines are easier to read and prevent code from becoming visually overwhelming. ### 7.4 Whitespace * **Do This:** Use consistent whitespace for indentation, spacing around operators, and blank lines to separate logical code blocks. """crystal def my_method(arg1 : Int32, arg2 : String) if arg1 > 10 puts "arg1 is greater than 10" else puts "arg1 is not greater than 10" end end """ * **Don't Do This:** Use inconsistent whitespace, making code harder to read. * **Why:** Consistent whitespace makes code visually appealing and easier to understand. This document provides a foundation for writing high-quality Crystal code by establishing clear architectural guidelines, enforcing consistent formatting, and emphasizing security considerations. By adhering to these standards, development teams can greatly improve the maintainability, performance, and overall quality of their Crystal projects.
# State Management Standards for Crystal This document outlines the coding standards for state management in Crystal projects. It aims to provide guidance on how to effectively manage application state, data flow, and reactivity, ensuring maintainability, performance, and security. These standards should be followed by all developers contributing to Crystal projects and serve as context for AI coding assistants. ## 1. Principles of State Management in Crystal ### 1.1. Understanding State State encompasses all the data that defines the current condition or situation of an application at any given time. Managing this state accurately and efficiently is crucial for the correct behavior of any application. * **Why it Matters:** Poorly managed state can lead to unpredictable behavior, bugs that are hard to track, and scalability issues. ### 1.2. Local vs. Global State * **Local State:** State confined to a specific component or module. It is typically simpler to manage and has less impact on the rest of the application. * **Global State:** State that is accessible and modifiable from anywhere in the application. Requires careful management to avoid conflicts and performance bottlenecks. ### 1.3. Immutability vs. Mutability * **Immutability:** Once an object is created, its state cannot be changed. Promotes predictability and simplifies debugging. * **Mutability:** The state of an object can be changed after creation. Requires careful synchronization to avoid race conditions and data corruption in concurrent environments. ## 2. Managing Global Application State ### 2.1. The Singleton Pattern (Use Sparingly) The Singleton pattern ensures that a class has only one instance and provides a global point of access to it. While useful in certain scenarios, overuse can lead to tightly coupled code and difficulties in testing. In Crystal, this is very easy to implement. * **Do This:** Use sparingly for truly global resources like configuration managers or logging services. * **Don't Do This:** Avoid using Singletons for business logic or data storage. **Example:** """crystal class Configuration private @@instance : Configuration? getter setting1 : String getter setting2 : Int32 private def initialize # Load configuration from file or environment @setting1 = "Default Value" @setting2 = 42 end def self.instance : Configuration @@instance ||= new end end # Usage config = Configuration.instance puts config.setting1 # Output: Default Value """ * **Why it Matters:** Controls access to a shared resource and prevents multiple instances. However, Singletons can make code harder to test and reason about. ### 2.2. Dependency Injection (Recommended) Dependency Injection (DI) provides dependencies to a component rather than having the component create or obtain them itself. This promotes loose coupling, testability, and reusability. * **Do This:** Favor DI for managing global state dependencies, especially in larger applications. Use constructor injection, method injection, or property injection. * **Don't Do This:** Avoid hardcoding dependencies within classes. * **Consider This:** The "lucky_di" shard is common for DI in Crystal. However, often simple manual injection is sufficient and preferred due to the simplicity of Crystal's object model. **Example:** """crystal class DatabaseConnection def query(sql : String) : Array(Hash(String, String)) # Simulate a database query puts "Executing query: #{sql}" [{"id" => "1", "name" => "Example"}] end end class UserRepository getter db_connection : DatabaseConnection def initialize(@db_connection : DatabaseConnection) end def get_user(id : Int32) : Hash(String, String)? results = @db_connection.query("SELECT * FROM users WHERE id = #{id}") results.first end end # Usage db = DatabaseConnection.new user_repo = UserRepository.new(db) user = user_repo.get_user(1) puts user # Output: Some({"id" => "1", "name" => "Example"}) """ * **Why it Matters:** Reduces coupling, improves testability, and makes it easier to swap out dependencies. ### 2.3. Configuration Objects Centralizing configuration parameters into configuration objects provides a convenient and organized way to manage application settings. * **Do This:** Load configuration from environment variables, configuration files (e.g., YAML, JSON), or command-line arguments during application startup. Use "ENV" for simple environment variable access. Consider shards like "config" or "kemal-config" for loading from files. * **Don't Do This:** Hardcoding configuration values directly in the code. **Example:** """crystal require "yaml" struct DatabaseConfig property host : String property port : Int32 property username : String property password : String end struct AppConfig property database : DatabaseConfig property log_level : String end def load_config(file_path : String) : AppConfig yaml = YAML.parse_file(file_path) database_config = DatabaseConfig.new( host: yaml["database"]["host"].as_s, port: yaml["database"]["port"].as_i, username: yaml["database"]["username"].as_s, password: yaml["database"]["password"].as_s ) AppConfig.new( database: database_config, log_level: yaml["log_level"].as_s ) end # Usage config = load_config("config.yml") puts config.database.host # Output: localhost puts config.log_level # Output: debug """ * **Why it Matters:** Simplifies configuration management and allows for easy modification without changing the code. ## 3. Managing Local Component State ### 3.1. Explicit State Variables For simple component state, use explicit instance variables to store and manage the state within the component. * **Do This:** Declare instance variables to hold the necessary state. * **Don't Do This:** Rely on implicit state or complex data structures for simple state management. **Example:** """crystal class Counter getter count : Int32 def initialize(@count : Int32 = 0) end def increment @count += 1 end def decrement @count -= 1 end end # Usage counter = Counter.new counter.increment puts counter.count # Output: 1 counter.decrement puts counter.count # Output: 0 """ * **Why it Matters:** Provides clear visibility and control over component state. ### 3.2. State Machines For more complex component state, consider using a state machine to manage the different states and transitions between them. * **Do This:** Define states, events, and transitions explicitly. Consider using a dedicated library for state machine management (no widely adopted standard lib exists, requires custom implementation or very small shard). * **Don't Do This:** Managing complex state transitions with nested "if" statements or ad-hoc logic. **Example:** """crystal enum State Idle Loading Loaded Error end class DataFetcher getter state : State def initialize @state = State::Idle end def load_data @state = State::Loading begin # Simulate loading data sleep 1 @state = State::Loaded rescue => e puts "Error: #{e}" @state = State::Error end end def process_data case @state when State::Loaded puts "Processing data..." else puts "Data not loaded yet." end end end # Usage fetcher = DataFetcher.new puts fetcher.state # Output: Idle fetcher.load_data puts fetcher.state # Output: Loaded fetcher.process_data # Output: Processing data... """ * **Why it Matters:** Formalizes state transitions, making the code more predictable, maintainable, and easier to test. ### 3.3. Immutability and State Updates Favor immutability when possible to simplify state management and prevent unintended side effects. Use immutable data structures and functions that return new state rather than modifying existing state. Crystal's value types (Int, Float, Tuple, etc.) are immutable. * **Do This:** Use immutable data structures. When updating state, create a new object with the modified state. **Example:** """crystal record Point, x : Int32, y : Int32 do def move(dx : Int32, dy : Int32) : Point Point.new(x + dx, y + dy) end end # Usage p1 = Point.new(x: 1, y: 2) p2 = p1.move(3, 4) puts p1 # Output: Point(x: 1, y: 2) puts p2 # Output: Point(x: 4, y: 6) """ * **Why it Matters:** Reduces the risk of bugs caused by unexpected state changes and simplifies reasoning about the code. ### 3.4 Thread Safety Crystal is inherently thread-safe due to its enforced isolation between threads using channels. When sharing state you *must* explicitly share it via channels; shared memory with concurrent access is disallowed by the compiler, preventing many common data race conditions. * **Do This:** Make use of channels to communicate between threads and share access to resources. Favor sending copies of data over references in concurrent contexts. * **Don't Do This:** Directly share mutable state across threads without synchronization mechanisms. **Example:** """crystal require "concurrent" channel = Channel(Int32).new spawn do 10.times do |i| channel.send(i) sleep 0.1 end channel.close end loop do value = channel.receive break if value.nil? puts "Received: #{value}" end """ * **Why it Matters:** Without explicit thread-communication mechanisms like channels, unexpected behavior and data corruption can easily arise in multi-threaded programs. Crystal's design forces developers to be explicit about thread safety, increasing robustness. ## 4. Data Flow and Reactivity ### 4.1. Explicit Data Flow Ensure that data flows through the application in a clear and predictable manner. Avoid implicit data dependencies and hidden side effects. * **Do This:** Define explicit interfaces and contracts between components. **Example:** """crystal interface DataProvider def fetch_data : Array(String) end class APIDataProvider def fetch_data : Array(String) # Simulate fetching data from an API ["Data from API"] end end class DataProcessor def initialize(@data_provider : DataProvider) end def process_data : Array(String) data = @data_provider.fetch_data data.map { |item| item.upcase } end end # Usage api_provider = APIDataProvider.new processor = DataProcessor.new(api_provider) result = processor.process_data puts result # Output: ["DATA FROM API"] """ * **Why it Matters:** Makes it easier to understand how data is transformed and passed through the application. ### 4.2. Observables and Reactive Programming Although Crystal does not have native support for reactive programming frameworks like RxJS, you can implement similar patterns using channels and custom event handlers. * **Do This:** Create channels or custom event handlers using blocks/procs to react to state changes. This promotes a decoupled way of updating states based on external events or time. **Example:** """crystal require "concurrent" class Observable(T) getter subscribers : Array(Proc(T)?) def initialize @subscribers = [] of Proc(T)? end def subscribe(&block : Proc(T)) @subscribers << block end def unsubscribe(block : Proc(T)) @subscribers.delete(block) end def notify(value : T) @subscribers.each do |subscriber| subscriber.call(value) if subscriber end end end # Usage observable = Observable(Int32).new subscriber1 = ->(value : Int32) { puts "Subscriber 1: #{value}" } subscriber2 = ->(value : Int32) { puts "Subscriber 2: #{value * 2}" } observable.subscribe(&subscriber1) observable.subscribe(&subscriber2) observable.notify(10) # Output: # Subscriber 1: 10 # Subscriber 2: 20 observable.unsubscribe(subscriber1) observable.notify(20) # Output: # Subscriber 2: 40 """ * **Why it Matters:** Enables building responsive and event-driven applications. Very helpful when multiple components depend on a single piece of changing data. ### 4.3. Using Callbacks and Events for Decoupling Leverage callbacks and events to decouple components and allow them to react to state changes without direct dependencies. * **Do This:** Define event handlers and use callbacks to notify listeners of state updates. * **Don't Do This:** Directly modifying the state of other components within event handlers. **Example:** """crystal class Button alias ClickHandler = Proc(Nil) getter click_handlers : Array(ClickHandler) = [] of ClickHandler def on_click(&block : ClickHandler) @click_handlers << block end def click @click_handlers.each { |handler| handler.call } end end class UIUpdater def update_display puts "Display updated!" end end # Usage updater = UIUpdater.new button = Button.new button.on_click do updater.update_display end button.click # Output: Display updated! """ * **Why it Matters:** Promotes modularity and reduces coupling between components, making the code more flexible and easier to maintain. ## 5. Common Anti-Patterns and Mistakes ### 5.1. God Objects Avoid creating "God Objects" that manage too much state and logic within a single class. Break down large classes into smaller, more manageable components. * **Do This:** Apply the Single Responsibility Principle (SRP) and ensure each class has a clear and focused purpose. * **Don't Do This:** Adding unrelated responsibilities to a single class. ### 5.2. Global Mutable State Avoid using global mutable state whenever possible. It can lead to unpredictable behavior and make it difficult to reason about the code. * **Do This:** Favor local state or immutable data structures. When global state is necessary, use proper synchronization mechanisms to prevent race conditions. * **Don't Do This:** Directly modifying global variables from multiple threads without protection. ### 5.3. Tight Coupling Avoid tight coupling between components, where changes in one component require changes in other components. * **Do This:** Use dependency injection, interfaces, and events to decouple components. * **Don't Do This:** Directly accessing and modifying the internal state of other components. ### 5.4. Ignoring Thread Safety In concurrent applications, ignoring thread safety can lead to data corruption and crashes. * **Do This:** Always consider thread safety when working with shared state and use appropriate synchronization mechanisms (channels, locks, etc.). Understand Crystal's concurrency model. * **Don't Do This:** Assuming that code is automatically thread-safe without proper consideration. ## 6. Performance Considerations ### 6.1. Minimize State Updates Reduce the frequency of state updates to improve performance. Batch updates together or use techniques like debouncing or throttling to limit the number of updates. ### 6.2. Efficient Data Structures Use appropriate data structures for managing state. For example, use "Set" for storing unique values, "Hash" for fast lookups, and "StaticArray" for fixed-size arrays. Crystal's type inference often allows the compiler to optimize these for maximum speed. ### 6.3. Avoid Unnecessary Cloning Cloning large data structures can be expensive. Avoid cloning unless absolutely necessary. If you need to modify a copy of an object, consider using techniques like copy-on-write to defer the actual copying until necessary. ### 6.4 Pre-Allocation when possible When dealing with collections, try to pre-allocate memory to avoid re-allocations during runtime. The use of capacity hints can drastically improve performance when large changes are made in a loop. **Example:** """crystal array = Array(Int32).new(initial_capacity = 100) 100.times do |i| array << i end """ ## 7. Security Considerations ### 7.1. Validate Input Data Always validate input data before using it to update state. This can prevent vulnerabilities such as injection attacks and data corruption. ### 7.2. Sanitize Output Data Sanitize output data before displaying it to the user. This can prevent cross-site scripting (XSS) attacks. ### 7.3. Secure Storage of Sensitive Data Store sensitive data (e.g., passwords, API keys) securely. Use encryption, hashing, and secure storage mechanisms to protect sensitive information. Never store secrets in plain text in the code or configuration files. Use environment variables or dedicated secret management solutions. ### 7.4. Principle of Least Privilege Components should only have access to the state they need to perform their functions. Limit the scope of access to prevent unintended side effects and security vulnerabilities. ## 8. Conclusion Effective state management is crucial for building robust, maintainable, and scalable Crystal applications. By following these coding standards, developers can ensure that state is managed correctly, data flows predictably, and the application remains secure and performant. Embracing immutability, leveraging dependency injection, and understanding Crystal's concurrency model are key practices for successful Crystal development.
# Performance Optimization Standards for Crystal This document outlines performance optimization standards for Crystal applications. These standards aim to improve speed, responsiveness, and resource utilization. The guidelines are tailored to Crystal's features and ecosystem, focusing on modern best practices and avoiding common pitfalls. ## 1. Architectural Considerations ### 1.1 Choosing the Right Data Structures and Algorithms * **Do This:** Select data structures and algorithms appropriate for the task. Consider tradeoffs between memory usage and processing speed. Use profilers to identify performance bottlenecks and guide optimization efforts. * **Don't Do This:** Blindly use familiar data structures without considering their performance characteristics in the specific context. Avoid premature optimization without profiling. **Why:** The choice of data structure and algorithm greatly impacts performance. Poor choices lead to inefficient code, regardless of other optimizations. **Example:** """crystal # Efficiently counting word frequencies using a Hash def word_frequencies(text : String) : Hash(String, Int32) frequencies = Hash(String, Int32).new text.split.each do |word| word = word.downcase # normalize frequencies[word] = (frequencies[word] || 0) + 1 end frequencies end # Less efficient approach using an Array, especially as the text grows def word_frequencies_array(text : String) : Array(Tuple(String, Int32)) frequencies = [] of Tuple(String, Int32) words = text.split words.each do |word| word = word.downcase existing_index = frequencies.index { |tupl| tupl[0] == word } if existing_index frequencies[existing_index] = {word, frequencies[existing_index][1] + 1} else frequencies << {word, 1} end end frequencies end """ The "word_frequencies" method using "Hash" is significantly faster due to the O(1) average time complexity for lookups, compared to the "word_frequencies_array" which relies on "Array" with O(n) complexity for "index". ### 1.2 Concurrency and Parallelism * **Do This:** Leverage Crystal's lightweight concurrency features (fibers and channels) to handle I/O-bound tasks and parallelism where appropriate. Use the "spawn" keyword for concurrent execution. Use channels for safe communication between fibers. * **Don't Do This:** Overuse concurrency, as excessive context switching can degrade performance. Avoid sharing mutable state without proper synchronization mechanisms. **Why:** Concurrency improves responsiveness and throughput. Parallelism can reduce processing time on multi-core systems. **Example:** """crystal require "socket" def handle_client(client : TCPSocket) begin puts "Connection from #{client.remote_address}" loop do line = client.gets break if line.nil? response = "Echo: #{line}" # simulate some processing client.puts response end ensure client.close puts "Connection closed" end end server = TCPServer.new("0.0.0.0", 8080) puts "Server listening on port 8080" loop do client = server.accept spawn handle_client(client) # Handle each client in a separate fiber. end """ This example utilizes fibers to handle multiple client connections concurrently, preventing blocking behavior and improving server throughput. ### 1.3 Memory Management * **Do This:** Minimize memory allocations and deallocations, especially in performance-critical sections. Reuse objects where possible. Be mindful of large object allocations, which can cause pauses due to garbage collection. Use the object pool pattern for frequently used objects. Consider using "uninitialized: true" when allocating arrays where the contents will be immediately overwritten, to avoid unnecessary initialization. * **Don't Do This:** Create unnecessary copies of data. Ignore potential memory leaks (though Crystal's garbage collector mitigates many common leaks, excessive allocation still impacts performance). **Why:** Frequent memory operations can lead to significant overhead. Reducing allocations and reusing objects reduces the burden on the garbage collector. **Example:** """crystal # Object Pool Pattern class ReusableObject @@pool : Channel(ReusableObject) = Channel(ReusableObject).new def self.get : ReusableObject @@pool.receive || new end def release @@pool.send(self) end # instance variables for the object @data : String def initialize @data = "" end def process(input : String) @data = input.upcase puts @data end def clear @data = "" end end # Usage obj = ReusableObject.get obj.process("hello") obj.clear # Reset the object's state instead of discarding it obj.release # Return the object to the pool """ This pattern reduces the overhead of creating and destroying objects, particularly useful for frequently used objects in server contexts. ### 1.4 Use "GC.disable" with Caution * **Do This:** Consider using "GC.disable" temporarily if you're performing a large batch operation where new object allocation is minimal after the initial setup. **Ensure** you re-enable the GC ("GC.enable") as soon as the operation is complete. * **Don't Do This:** Use "GC.disable" globally or for extended periods, as this can lead to uncontrolled memory growth and eventual crashes if the GC is never re-enabled. **Why:** Disabling GC can reduce overhead during intensive operations, but it demands careful management. **Example:** """crystal GC.disable begin # Perform intensive operation with minimal new object allocation big_array = Array.new(1_000_000) { |i| i } # Process the array without creating many new objects big_array.each do |i| # Perform calculations here, minimizing allocations end ensure GC.enable # Ensure GC is re-enabled, even if an error occurs end """ ### 1.5 Profiling * **Do This:** Use profiling tools ("--profile", FlameScope, etc.) to identify performance bottlenecks. Analyze CPU usage, memory allocation, and time spent in different parts of your code. * **Don't Do This:** Rely on intuition or guesswork to optimize code without profiling data. **Why:** Profiling provides data-driven insights into performance issues, guiding optimization efforts effectively. **Example:** Run your Crystal program with profiling enabled: """bash crystal build your_program.cr --release --profile ./your_program """ Then use a tool like FlameScope to analyze the generated "profile.json" for CPU hotspots. ## 2. Code-Level Optimizations ### 2.1 Type Specialization * **Do This:** Take advantage of Crystal's static typing to allow the compiler to generate optimized machine code. Declare specific type constraints when possible. Use structs instead of classes when inheritance and polymorphism are not needed, as structs are stack-allocated and faster. Favor concrete types over abstract types where possible. * **Don't Do This:** Neglect type annotations, as this can hinder the compiler's ability to optimize. Overuse generic types if concrete types are known. **Why:** Static typing enables compile-time optimizations, reducing runtime overhead. **Example:** """crystal # Optimized with type specialization def add(x : Int32, y : Int32) : Int32 x + y end # Less optimized due to lack of type specificity (relies on Union type) def add_untyped(x, y) x + y # Compiler must perform runtime type checks if types are not known end """ The first "add" function allows the compiler to generate optimized machine code because types are known at compile time. The second function requires runtime type checks, which impacts performance. For best performance, inlined methods are best. ### 2.2 String Manipulation * **Do This:** Use "String#build" for efficient string concatenation, especially when dealing with a large number of concatenations. Prefer "String#[]=", "String#insert", and "String#<<" for in-place modifications when appropriate. Avoid unnecessary string allocations. Use "Slice" liberally for accessing substrings without copying. * **Don't Do This:** Use "+" or "String#concat" for repeated string concatenation within loops, as these create new string objects in each iteration. Create unnecessary copies of strings. **Why:** String operations are common in many applications, and inefficient string handling can be a source of performance bottlenecks. **Example:** """crystal # Efficient string concatenation def build_string(items : Array(String)) : String String.build do |io| items.each do |item| io << item end end end # Inefficient string concatenation def concat_string(items : Array(String)) : String result = "" items.each do |item| result += item # Creates a new string object in each iteration end result end """ The "build_string" function is much more efficient, especially for large arrays, because it avoids creating intermediate string objects. ### 2.3 Iteration and Looping * **Do This:** Use optimized iterators like "each", "each_with_index" when appropriate. When performance is critical and the number of iterations is known, use "(0...n).to_a.each" or iterative loops ("for i in 0...n") . Prefer "while" loops for simple iterative tasks. * **Don't Do This:** Use inefficient or unnecessarily complex looping constructs. Use "Array#size" repeatedly within a loop condition; cache the size instead. **Why:** Efficient iteration is crucial for processing collections of data. **Example:** """crystal # Efficient iteration using each array = [1, 2, 3, 4, 5] array.each do |element| puts element end # Inefficient: Repeatedly calling array.size within the loop condition array2 = [1, 2, 3, 4, 5] i = 0 while i < array2.size puts array2[i] i += 1 end # more efficient caching the size array3 = [1, 2, 3, 4, 5] size = array3.size i = 0 while i < size puts array3[i] i += 1 end """ ### 2.4 Inlining * **Do This:** Mark frequently called, small methods as "inline" to encourage the compiler to inline them. Profile your code to identify candidate methods for inlining. * **Don't Do This:** Inline large or infrequently called methods, as this can increase code size without providing a significant performance benefit. **Why:** Inlining reduces function call overhead, improving performance. **Example:** """crystal # Inlined method for fast access inline def square(x : Int32) : Int32 x * x end result = square(5) # The compiler may replace this call with the actual code of square """ ### 2.5 Allocation-Free Operations * **Do This:** Explore allocation-free APIs where available, especially in tight loops. For example use "IO::Memory" instead of creating strings. * **Don't Do This:** Unnecessarily allocate memory which can lead to GC pressure. **Why:** Reducing memory allocation is an important optimization since it reduces garbage collection overhead. **Example:** """crystal require "io" data = "some data" IO::Memory.new(data) do |io| # The underlying memory of "data" is directly read from, without any memory allocation. io.each_line do |line| puts line end end """ ### 2.6 Optimize Regular Expressions * **Do This:** Compile regular expressions ahead of time if they are used repeatedly. Use appropriate regex flags for performance. Consider simpler string operations if possible. * **Don't Do This:** Create and compile regular expressions repeatedly inside loops. Use overly complex regular expressions when simpler patterns suffice. **Why:** Regular expression compilation can be expensive. Optimized regex patterns improve matching speed. **Example:** """crystal # Compile regex once and reuse @@regex = /pattern/.freeze def process_data(data : String) if @@regex.match(data) puts "Match found" end end """ ### 2.7 Avoid Excessive Metaprogramming * **Do This:** Use metaprogramming judiciously to avoid runtime performance penalties if possible. * **Don't Do This:** Overuse metaprogramming when simpler, more direct approaches are available. **Why:** Metaprogramming can introduce runtime overhead due to dynamic code generation and evaluation. Compile-time metaprogramming with macros is generally much more performant. ### 2.8 Struct vs Class * **Do This:** Prefer "struct" when you have a value type that does not require inheritance, shared mutable state, or object identity. * **Don't Do This:** Use "class" when a "struct" would suffice. * **Why:** Structs are allocated on the stack - leading to faster allocation and deallocation and smaller memory footprint. Classes introduces GC overhead. **Example:** """crystal # Prefer Struct struct Point x : Int32 y : Int32 end # Avoid Class if structs work class PointClass x : Int32 y : Int32 end """ ### 2.9 C Bindings for Compute Intensive Tasks * **Do This:** If bottlenecks remain, consider using C bindings if Crystal implementation is slower than an existing C library. * **Don't Do This:** Use C bindings for tasks that Crystal can handle efficiently by itself. Prefer optimized Crystal code over unoptimized C code * **Why:** This allows you to leverage well-optimized C libraries for specific algorithms or functionalities. **Example:** """crystal lib SomeCLibrary fun c_function(arg : Int32) : Int32 end def crystal_function(arg : Int32) : Int32 SomeCLibrary.c_function(arg) end """ Be aware of the overhead incurred when crossing the C/Crystal boundary. ## 3. IO Optimization ### 3.1 Buffering * **Do This:** Use buffered IO operations when dealing with files and network connections. * **Don't Do This:** Perform unbuffered IO for large data streams, resulting in excessive system calls. **Why:** Buffering reduces the number of system calls. **Example:** """crystal File.open("large_file.txt", "r") do |file| buffered_reader = IO::Buffered::Reader.new(file) while line = buffered_reader.gets # Process line end end """ ### 3.2 Zero-Copy Techniques * **Do This:** Explore zero-copy techniques where possible, for tasks like sending files over a network socket. * **Don't Do This:** Create unnecessary copies of data when transferring data between IO streams. **Why:** Copying large chunks of data can add significant overhead. """crystal require "socket" def send_file(socket : TCPSocket, file_path : String) File.open(file_path, "r") do |file| socket.send_file(file) # Zero-copy send (if supported by the OS/socket) end end """ ## 4. Database Interactions ### 4.1 Connection Pooling * **Do This:** Use connection pooling to reuse database connections and reduce connection overhead. Consider libraries like "connection_pool" if appropriate. * **Don't Do This:** Create a new database connection for each request. **Why:** Establishing new database connections is an expensive operation. ### 4.2 Prepared Statements * **Do This:** Use prepared statements to prevent SQL injection and improve query performance. * **Don't Do This:** Concatenate user input directly into SQL queries. **Why:** Prepared statements are precompiled and can be executed multiple times with different parameters, avoiding recompilation overhead. ### 4.3 Data Serialization * **Do This:** Use efficient data serialization formats like Protocol Buffers, Avro, or MessagePack for inter-service communication or data storage. * **Don't Do This:** Use inefficient formats like JSON where binary formats are more appropriate. **Why:** Binary serialization formats are typically faster and produce smaller payloads. ## 5. Compiler Flags and Build Options ### 5.1 Release Mode Compilation * **Do This:** Always compile your code in release mode ("crystal build --release ...") for production deployments. * **Don't Do This:** Deploy debug builds to production, as they contain extra debugging information and lack optimizations. **Why:** Release mode enables optimizations, such as inlining and dead code elimination, which can significantly improve performance. ### 5.2 Target CPU Architecture * **Do This:** Specify the target CPU architecture ("crystal build --target=native ..." or "--target=x86_64-linux-gnu") to enable architecture-specific optimizations. * **Don't Do This:** Use generic build targets when deploying to specific hardware. **Why:** Architecture-specific optimizations can improve performance on the target hardware. ### 5.3 Link-Time Optimization (LTO) * **Do This:** Consider enabling LTO ("-Dlink_time_optimization") during compilation for potentially further optimizations involving interactions *between* compiled parts of your program. * **Don't Do This:** Use "LTO" during development because compilation will be slower. **Why:** LTO performs whole-program optimization at link time, which can result in better overall performance. ## 6. Caching ### 6.1 Result Caching * **Do This:** Cache expensive function results (e.g., database queries, complex calculations) to avoid redundant computations. Use memoization techniques for pure functions. * **Don't Do This:** Cache results indefinitely without considering cache invalidation. **Why:** Caching reduces the need to recompute results. **Example:** """crystal require "memoize" class DataFetcher include Memoize def fetch_data(query : String) : String # ... expensive database query ... sleep 1 # Simulate expensive action "Data for #{query}" end memoize :fetch_data end fetcher = DataFetcher.new puts fetcher.fetch_data("query1") # First call - takes time puts fetcher.fetch_data("query1") # Second call - returns cached result immediately """ ### 6.2 HTTP Caching * **Do This:** Implement HTTP caching (e.g., using "Cache-Control" headers) to reduce server load and improve client-side performance. Use a reverse proxy cache (e.g., Varnish, Nginx) to cache responses close to the client. * **Don't Do This:** Neglect HTTP caching, especially for static assets and frequently accessed API endpoints. **Why:** Browser caching and proxy caching reduce network traffic and server load. ## 7. Monitoring and Continuous Improvement ### 7.1 Performance Monitoring * **Do This:** Implement performance monitoring using tools like Prometheus, Grafana, or StatsD to track key metrics (e.g., response time, throughput, error rates). * **Don't Do This:** Operate in the dark without performance metrics. **Why:** Monitoring provides insights into application performance and helps identify areas for improvement. ### 7.2 Continuous Profiling * **Do This:** Regularly profile your application in production to identify performance regressions and new bottlenecks. * **Don't Do This:** Profile only during development. Production profiles offer a more realistic view of performance characteristics. **Why:** Production traffic patterns and data volumes can reveal performance issues that are not apparent in development environments. By adhering to these performance optimization standards, Crystal developers can build high-performance applications that are efficient, responsive, and scalable. Remember to prioritize profiling, testing, and continuous monitoring to ensure ongoing performance improvements.