#builder-pattern #type-safe #di #ioc #dependency-injection

build service-builder

A lightweight, type-safe service construction library for Rust that provides compile-time dependency injection through builder pattern

6 releases

0.2.2 Jan 12, 2025
0.2.1 Jan 11, 2025
0.1.2 Jan 9, 2025
0.1.1 Dec 28, 2024

#336 in Rust patterns

Download history 67/week @ 2024-12-11 59/week @ 2024-12-18 128/week @ 2024-12-25 6/week @ 2025-01-01 444/week @ 2025-01-08 50/week @ 2025-01-15

628 downloads per month

MIT license

11KB

service-builder

Crates.io Documentation License: MIT

A lightweight, type-safe service construction library for Rust that leverages the builder pattern to provide a more idiomatic alternative to traditional dependency injection.

Features

  • 🔒 Type-safe dependency injection at compile time
  • 🚀 Zero runtime overhead - everything is checked at compile time
  • 🛠️ Automatic builder implementation via proc-macros
  • 📦 Field-level getters and setters with attribute control
  • Zero-cost abstractions - no runtime reflection or dynamic dispatch
  • 🔍 Comprehensive error handling with descriptive messages

Why Builder Pattern in Rust?

1. Ownership and Borrowing

Traditional dependency injection frameworks often struggle with Rust's ownership system. The builder pattern works naturally with Rust's ownership rules:

// ❌ Traditional DI approach - fights with ownership
container.register::<UserService>(UserService::new);
let service = container.resolve::<UserService>().unwrap(); // Runtime checks

// ✅ Builder pattern - works with ownership
let service = UserService::builder()
    .repository(repo)
    .cache(cache)
    .build()?; // Compile-time checks

2. Compile-Time Guarantees

Rust's type system can catch dependency issues at compile time with the builder pattern:

#[builder]
struct UserService {
    repository: Arc<dyn Repository>,
    cache: Arc<dyn Cache>,
}

// Won't compile if you forget a dependency
let service = UserService::builder()
    .repository(repo)
    // Forgot .cache()
    .build(); // Compile error!

3. Clear Dependency Flow

Dependencies are explicit and visible in the code:

let auth_service = AuthService::builder()
    .user_repository(user_repo)
    .token_service(token_service)
    .build()?;

let post_service = PostService::builder()
    .post_repository(post_repo)
    .auth_service(auth_service) // Clear dependency chain
    .build()?;

Quick Start

Add this to your Cargo.toml:

[dependencies]
service-builder = "0.2.0"

Basic Usage with Builder Pattern

use service_builder::prelude::*;
use std::sync::Arc;

#[builder]
struct UserService {
    repository: Arc<dyn UserRepository>,
    cache: Arc<dyn Cache>,
}

let user_service = UserService::builder()
    .repository(user_repo)
    .cache(cache)
    .build()?;

Using Getters and Setters

You can add getter and setter methods to your fields using attributes:

#[builder]
struct Config {
    #[builder(getter)]  // Generates get_api_key()
    api_key: String,
    
    #[builder(setter)]  // Generates set_timeout()
    timeout: Duration,
    
    #[builder(getter, setter)]  // Generates both
    max_retries: u32,
}

let mut config = Config::builder()
    .api_key("secret".to_string())
    .timeout(Duration::from_secs(30))
    .max_retries(3)
    .build()?;

// Use generated getter
assert_eq!(config.get_api_key(), &"secret".to_string());

// Use generated setter
config.set_max_retries(5);

Composing Services

#[builder]
struct AppServices {
    #[builder(getter)]  // Access services via getters
    user_service: Arc<UserService>,
    post_service: Arc<PostService>,
}

let app_services = AppServices::builder()
    .user_service(Arc::new(user_service))
    .post_service(Arc::new(post_service))
    .build()?;

// Access services using generated getters
let user_service = app_services.get_user_service();

Builder Pattern vs Traditional DI

Advantages of Builder Pattern

  1. Type Safety: All dependencies are checked at compile time
  2. Zero Runtime Cost: No reflection or dynamic dispatch overhead
  3. Ownership Control: Works naturally with Rust's ownership system
  4. Explicit Dependencies: Dependencies are clearly visible in the code
  5. Flexible Access: Optional getter/setter generation for fine-grained control

Disadvantages of Traditional DI

  1. Runtime Overhead: Container resolution and type checking at runtime
  2. Safety Issues: Potential runtime panics from missing dependencies
  3. Ownership Complexity: DI frameworks often struggle with Rust's ownership rules
  4. Hidden Dependencies: Dependencies are often hidden in container configuration
  5. Runtime Failures: Many dependency issues only surface at runtime

Contributing

We welcome contributions! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Dependencies

~0.6–1MB
~23K SLoC