Overview
sabi was developed with the goal of thoroughly separating business logic from data access. However, it differs from conventional Dependency Injection (DI) frameworks that merely invert dependencies by placing an interface between the two layers—sabi draws a clear line beyond that simple approach.
What elevates sabi to an advanced framework in particular are the following two key techniques: introducing a data-access interface optimized for each individual piece of logic, and routing input/output from the controller layer directly to the data access layer via DataSrc, completely bypassing the logic layer.
Introducing Data-Access Traits Optimized Per Logic Unit
The former approach thoroughly embodies the Interface Segregation Principle (ISP)—one of the SOLID principles that has often ended up more nominal than real in practice. Each piece of logic (use case) defines, on the logic side, its own dedicated trait (interface) that specifies only the operations it truly needs. Meanwhile, the data access side implements traits based on its responsibility as a data provider. The DataHub then mediates and maps between the two, so that the logic side never needs to be aware of the data access side's structure, and the data access side never needs to depend on the structure of individual pieces of logic—each maintains its own independent responsibility.
This design is grounded in the philosophy that "the world does not exist as a single, fixed, objective reality, but rather reveals its meaning and form according to the questions and purposes held by the observing subject." The trait that logic should see should not be dictated by the constraints of the data access side, but should instead be defined based on the logic's own context and needs. The same holds true in reverse for the trait that data access should see. What matters to the logic is not the structure of the database or the ORM, but the "capability required to realize this particular use case." What matters to the data access side, on the other hand, is how to access storage or external services. By having both sides define their traits according to their own respective contexts, and having the DataHub bridge them together, true loose coupling is achieved—one where neither side depends on the other's internal structure.
Furthermore, because logic never needs to know any implementation details of data access, it can easily be swapped out for mocks that provide the necessary capabilities during testing, achieving high testability as well.
A Structure That Routes Controller-Layer I/O Directly to the Data Access Layer, Bypassing the Logic Layer
The latter approach—routing input/output from the controller layer directly to the data access layer, completely bypassing the logic layer—was conceived by decomposing the controller layer's role into two elements: "invoking logic" and "input/output data." In conventional architectures, because these two elements were never separated, a layered structure whose sole responsibility was data flow (the so-called "data bucket brigade") became unavoidable, forcing data to be transformed every time it crossed a layer. However, by routing input/output data directly to the infrastructure layer (the data access division), this redundant data flow—and the layered structure that existed to support it—becomes entirely unnecessary.
As a result, the hierarchical dependency that would normally remain between logic and data access is completely eliminated, and is elevated instead into an equal relationship based on a contract defined by the trait's method signatures. This can be seen as an evolutionary extension of the Dependency Inversion Principle (DIP)—taking the conventional DIP, which merely reverses the direction of dependency, a step further by minimizing and localizing the dependency itself within the boundary of the contract.
An AI-Friendly, Capability-Oriented Framework
Moreover, this structure is also well-suited to AI-driven automated programming. For AI-driven automated programming to be performed safely and accurately, the following conditions are required:
- Localized dependencies: The scope of dependencies an AI must grasp for a single change should be confined to a narrow portion of the system, not the system as a whole.
- Explicit boundaries of side effects: The extent to which side effects can propagate should be made explicit in the code itself, rather than relying on implicit call ordering.
- Localized impact of change: Adding features or swapping implementations should not ripple out into existing code that doesn't use them.
- Substitutability through contract: An implementation should be safely replaceable as long as it satisfies the contract (trait), without needing to know its implementation details.
sabi satisfies all of these conditions at the structural level by defining a dedicated data access trait for each piece of logic and making that logic depend only on that narrow trait—so that adding functionality to a particular DataAcc never affects existing logic, and swapping implementations can be done safely as long as the trait's contract is satisfied. In this way, sabi is not merely a DI framework, but an AI-friendly, capability-oriented framework suited to the modern AI era.
Install
In Cargo.toml, write this crate as a dependency:
[]
= "0.8.0" # For synchronous APIs
For asynchronous APIs with tokio runtime, enable the tokio feature:
[]
= { = "0.8.0", = ["tokio"] }
= { = "1", = ["full"] } # Required for tokio runtime
Usage (Synchronous)
1. Implementing a logic function and a data access trait
First, define a function that represents your application logic, along with its dedicated data access trait.
This trait is independent of specific data source implementations, improving testability.
The #[overridable] macro is used to allow this trait implementation to be overridden later.
use overridable;
2. Implementing DataAcc derived traits
The DataAcc trait provides a simple mechanism to retrieve DataConn objects.
However, it's the derived traits (like GettingDataAcc and SettingDataAcc in this example) that define the application-specific methods for accessing data.
These methods then use DataAcc::get_data_conn to obtain the appropriate DataConn and perform the actual data operations.
The #[overridable] macro is also used here to allow these methods to be integrated with DataHub.
use DataAcc;
use overridable;
use RedisDataConn;
use StdioDataConn; // This is a conceptual, non-existent DataConn.
3. Integrating data traits and DataAcc derived traits into DataHub
The DataHub is the central component that manages all DataSrc and DataConn, providing access to them for your application logic.
It implements Send.
By implementing the data traits (MyData) from step 1. and the DataAcc traits from step 2. on DataHub, you integrate them.
The #[override_with] macro indicates that the methods of the MyData trait will be provided by the corresponding methods of the DataAcc derived traits.
use DataHub;
use override_with;
use crateMyData;
use crate;
4. Using logic functions and DataHub
Inside your main function, register a global DataSrc and setup the sabi framework.
Then, create an instance of DataHub and register the necessary local DataSrc using the uses method.
Finally, use the run method or txn method of DataHub to execute your defined application logic function (my_logic) without or within a transaction.
use ;
use RedisDataSrc;
use StdioDataSrc; // This is a conceptual, non-existent DataConn.
use cratemy_logic;
use ExitCode;
// Register global DataSrc using the `sabi::uses!` macro.
// This makes `RedisDataSrc` available throughout the application.
uses!;
Usage (Asynchronous)
When the tokio feature is enabled, sabi-rust provides asynchronous counterparts for its core components and methods, allowing you to build non-blocking data access layers with the Tokio runtime.
The asynchronous APIs are available under the sabi::tokio module.
1. Implementing logic functions and data traits (Asynchronous)
Your application logic functions and their associated traits will now be async.
The #[overridable] macro still functions the same way to allow trait implementations to be overridden.
Note that if the logic function is executed within a spawned task (e.g., via tokio::spawn), the data access trait argument must also implement Send.
This is because the future returned by the logic function captures the argument and must itself be Send.
use overridable;
pub async
2. Implementing DataAcc derived traits (Asynchronous)
The DataAcc trait and its derived traits will now also use async methods and rely on DataAcc::get_data_conn_async to retrieve asynchronous data connections.
use DataAcc;
use overridable;
use RedisDataConnAsync;
use StdioDataConnAsync; // This is a conceptual, non-existent DataConn.
3. Integrating data traits and DataAcc derived traits into DataHub (Asynchronous)
The sabi::tokio::DataHub serves the same central role, but operates asynchronously.
It also implements Send.
The integration with traits using #[override_with] remains conceptually similar.
use DataHub;
use override_with;
use crateMyAsyncData;
use crate;
4. Using logic functions and DataHub (Asynchronous)
Use the #[tokio::main] macro to run your main asynchronous function.
Register global DataSrc using sabi::tokio::uses!.
Set up the framework with sabi::tokio::setup_async.
Execute your logic with data.txn_async or data.run_async.
use ;
use tokio; // Ensure tokio is in scope for #[tokio::main]
use RedisDataSrcAsync;
use StdioDataSrcAsync;
use cratemy_async_logic;
// Register global DataSrc using the `sabi::tokio::uses!` macro.
uses!;
async
async
Related Links
Data Sources
- sabi_redis (Rust) ... The DataSrc implementation for Redis
Implementations in other languages
- sabi (Golang) ... The sabi implementation in Go
- sabi (Java) ... The sabi implementation in Java
Supported Rust versions
This crate supports Rust 1.87.0 or later.
)
License
Copyright (C) 2024-2026 Takayuki Sato
This program is free software under MIT License. See the file LICENSE in this distribution for more details.