Thread-Safe Architecture Using Arc<T> for Infrastructure Dependencies in Forge Services
Forge services achieve thread-safe sharing by wrapping every infrastructure dependency in an Arc<T>, enabling cheap cloning across async tasks without requiring mutable references or complex synchronization primitives.
The antinomyhq/forgecode codebase implements a rigorous thread-safe architecture using Arc<T> for infrastructure dependencies that allows services to be duplicated for each request while maintaining safe concurrent access. By atomically reference-counting external resources like repositories and configuration providers, Forge eliminates copy overhead and avoids borrow checker constraints. This design follows clean-architecture guidelines documented in AGENTS.md and appears throughout the service layer in crates/forge_services.
Why Arc for Shared Ownership
Arc<T> (atomically reference-counted pointer) provides shared ownership across threads without needing mutable references. Cloning an Arc only increments a reference counter, making it cheap to duplicate services for concurrent tasks.
When interior mutability is required, Arc<T> composes cleanly with RwLock<T> or Mutex<T>, keeping the public API free of &mut constraints. This pattern ensures that any number of concurrent tasks can hold a clone of the same service and safely call its async methods.
The Service Pattern: Arc Storage
Each Forge service stores its infrastructure as a single Arc<T> field and exposes a constructor that takes ownership of the arc. Notably, these constructors apply no trait bounds, following the clean-architecture guideline that constraints belong only on methods that need them.
In crates/forge_services/src/provider_service.rs, the ForgeProviderService wraps a repository:
/// Store the repository in an `Arc` for cheap cloning.
pub struct ForgeProviderService<R> {
repository: Arc<R>,
}
impl<R> ForgeProviderService<R> {
/// Constructor takes ownership of an `Arc<R>`.
pub fn new(repository: Arc<R>) -> Self {
Self { repository }
}
}
Source: provider_service.rs lines 16–18
Tuple-Struct Pattern for Single Dependencies
When a service has exactly one dependency, Forge adopts the tuple-struct pattern to eliminate boilerplate while keeping the dependency explicit and clone-friendly.
In crates/forge_services/src/tool_services/plan_create.rs, ForgePlanCreate uses this approach:
/// Single-dependency service using the tuple-struct pattern.
pub struct ForgePlanCreate<F>(Arc<F>);
impl<F> ForgePlanCreate<F> {
pub fn new(infra: Arc<F>) -> Self { Self(infra) }
}
Source: plan_create.rs lines 15–18
This shorthand stores the Arc<F> directly in the struct tuple, requiring no named field accessors while maintaining the same thread-safe semantics.
Composing High-Level Services with Arc
Higher-level services compose lower-level ones simply by storing Arc<…> for each component. The top-level ForgeServices struct in crates/forge_services/src/forge_services.rs demonstrates this aggregation:
pub struct ForgeServices<F> {
// … many other services omitted for brevity …
plan_create_service: Arc<ForgePlanCreate<F>>,
file_read_service: Arc<ForgeFsRead<F>>,
// … more services …
}
impl<F> ForgeServices<F> {
pub fn new(infra: Arc<F>) -> Self {
Self {
plan_create_service: Arc::new(ForgePlanCreate::new(infra.clone())),
file_read_service: Arc::new(ForgeFsRead::new(infra)),
// … initialise others …
}
}
}
Source: forge_services.rs lines 58–77
Each sub-service receives a cloned Arc, ensuring that the entire service tree shares the same infrastructure reference safely across threads.
Concurrent Usage in Async Tasks
Because every service is cloneable via Arc, spawning concurrent tasks requires no additional synchronization code:
let infra = Arc::new(MyInfrastructure::new());
let provider_service = Arc::new(ForgeProviderService::new(infra.clone()));
tokio::join!(
async { provider_service.clone().some_async_op().await },
async { provider_service.clone().another_async_op().await },
);
Each async task holds its own Arc clone, guaranteeing that the underlying infrastructure is dropped only when the last task completes.
Summary
- Arc enables shared ownership of infrastructure across threads without copying data or requiring
&mutreferences. - Service constructors accept
Arc<T>directly with no trait bounds, delaying constraints to method implementations. - Tuple-struct pattern (
struct Service<T>(Arc<T>)) provides minimal boilerplate for single-dependency services. - High-level composition aggregates multiple
Arc-wrapped services, as shown inForgeServices. - Testability improves because mock infrastructure can be wrapped in
Arc::new(MockImpl{})and injected identically to production code.
Frequently Asked Questions
What is the benefit of using Arc over other synchronization primitives?
Arc<T> provides reference counting without locking, making it ideal for immutable data or cases where interior mutability is handled separately. Unlike Mutex<T> or RwLock<T>, Arc itself does not synchronize access to the data—it only tracks ownership. This allows Forge services to be cloned cheaply (just an atomic increment) while still being Send + Sync when the underlying type permits.
How does Forge handle interior mutability when using Arc?
When a service requires mutation, Forge wraps the data inside interior mutability primitives like RwLock<T> or Mutex<T> inside the Arc. For example, Arc<RwLock<Config>> allows thread-safe mutation while the Arc handles ownership. This keeps the service struct itself free of lifetime or mutability constraints, simplifying the public API.
Why does Forge use tuple-structs for some services?
The tuple-struct pattern (struct Service(Arc<T>)) eliminates field-naming boilerplate when a service has exactly one dependency. According to AGENTS.md, this convention maintains explicit dependency injection while reducing code size. It is used in files like plan_create.rs for services that act as thin wrappers around a single infrastructure trait.
How does this architecture improve testability?
Because services depend on Arc<T> rather than concrete types, unit tests can inject mock implementations wrapped in Arc::new(MockImpl{}). The service under test receives the same runtime behavior—cheap cloning and thread-safe access—as it would with production infrastructure, allowing tests to run concurrently without race conditions or complex setup.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →