Skip to content

sttk/sabi-java

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

81 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sabi

"sabi" - A small framework to separate logic and data access


Maven Central MVN Repository GitHub.io CI Status MIT License

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 Interfaces 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 interface that specifies only the operations it truly needs. Meanwhile, the data access side implements interfaces 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 interface 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 interface 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 interfaces 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 interface'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 (interface), without needing to know its implementation details.

sabi satisfies all of these conditions at the structural level by defining a dedicated data access interface for each piece of logic and making that logic depend only on that narrow interface—so that adding functionality to a particular DataAcc never affects existing logic, and swapping implementations can be done safely as long as the interface'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

This package can be installed from Maven Central Repository.

The examples of declaring that repository and the dependency on this package in Maven pom.xml and Gradle build.gradle are as follows:

For Maven

  <dependencies>
    <dependency>
      <groupId>io.github.sttk</groupId>
      <artifactId>sabi</artifactId>
      <version>0.8.0</version>
    </dependency>
  </dependencies>

For Gradle

repositories {
  mavenCentral()
}
dependencies {
  implementation 'io.github.sttk:sabi:0.8.0'
}

Usage

1. Implementing a logic function and a data access interface

First, define a functional interface that represents your application logic, along with its dedicated data access interface. This interface is independent of specific data source implementations, improving testability.

import com.github.sttk.errs.Err;

interface MyData {
  String getText() throws Err;
  void setText(String text) throws Err;
}
import com.github.sttk.errs.Err;

class MyLogic implements Logic<MyData> {
  @Override
  public void run(MyData data) throws Err {
    String text = data.getText();
    data.setText(text);
  }
}

2. Implementing DataAcc derived interfaces

The DataAcc interface provides a simple mechanism to retrieve DataConn objects. However, it's the derived interfaces (like GettingDataAcc and SettingDataAcc in this example) that define the application-specific methods for accessing data. These methods then use DataHub#getDataConn to obtain the appropriate DataConn and perform the actual data operations.

interface AllLogicData extends MyData {}
import com.github.sttk.errs.Err;
import com.github.sttk.sabi.DataAcc;

interface GettingDataAcc extends DataAcc, AllLogicData {
  default String getText() throws Err {
    return "output text";
  }
}
import com.github.sttk.errs.Err;
import com.github.sttk.sabi.DataAcc;
import com.github.sttk.sabi_redis.RedisDataConn;
import com.github.sttk.sabi_stdio.StdioDataConn;  // This is a conceptual, non-existent DataConn.
import java.io.InputStream;
import java.io.PrintStream;

interface SettingDataAcc extends DataAcc, AllLogicData {
  default void setText(String text) throws Err {
    var redisDc = getDataConn("redis", RedisDataConn.class);
    var redisConn = redisDc.getConnection();
    var commands = redisConn.sync();
    try {
      commands.set("sample", text);
    } catch (Exception e) {
      throw new Err("fail to set value", e);
    }

    redisDc.addRollback(rConn -> {
      var cmd = rConn.sync();
      try {
        cmd.del("sample");
      } catch (Exception e) {
        throw new Err("fail to roll back", e);
      }
    });

    var stdioDc = getDataConn("stdio", StdioDataConn.class);
    stdioDc.addPostCommit((InputStream in, PrintStream out, PrintStream err) -> {
      out.printf("%s", text);
    });
  }
}

3. Integrating data interfaces and DataAcc derived interfaces into DataHub

The DataHub is the central component that manages all DataSrc and DataConn, providing access to them for your application logic. By implementing the data interface (MyData) from step 1. and the DataAcc classes from step 2. on DataHub, you integrate them.

import com.github.sttk.sabi.DataHub;

class MyDataHub extends DataHub implements GettingDataAcc, SettingDataAcc {
  public MyDataHub() {}
}

4. Using logic functions and DataHub

Inside your static block, register your global DataSrc. Next, main function calls run function, and inside run function, set up the sabi framework. Then, create an instance of DataHub and register the necessary local DataSrc using the Sabi.uses method. Finally, use the DataHub#run method or DataHub#txn method to execute your defined application logic function (MyLogic) without or within a transaction.

import com.github.sttk.errs.Err;
import com.github.sttk.sabi.DataHub;
import com.github.sttk.sabi.Sabi;

public class Main {
  static {
    // Register global DataSrc.
    Sabi.uses("foo", new FooDataSrc());
  }

  static MyLogic myLogic = new MyLogic();

  public static void main(String[] args) {
    try {
      run();
    } catch (Exception e) {
      System.err.println(e.toString());
      System.exit(1);
    }
  }

  static void run() throws Err {
    // Set up the sabi framework
    try (var ac = Sabi.setup()) {
      
      // Creates a new instance of DataHub.
      try (var data = new MyDataHub()) {

        // Register session-local DataSrc with DataHub
        data.uses("bar", new BarDataSrc());

        // Execute application logic without a transaction control. 
        data.run(myLogic);

        // If you need to execute logic within a transaction, use the `txn` method instead of `run`
        // data.txn(myLogic);
      }
    }
  }
}

Native build

This framework supports native build with GraalVM.

See the following pages to setup native build environment on Linux/macOS or Windows.

And see the following pages to build native image with Maven or Gradle.

Since this framework does not use Java reflections, etc., any native build configuration files are not needed.

And all logic and data access implementations should not use them, too. However, some of client libraries provided for data sources might use them, and it might be needed those configuration files.

Supporting JDK versions

This framework supports JDK 21 or later.

Actually checked JDK versions:

  • Oracle GraalVM 25.0.1+8.1 (build 25.0.1+8-LTS-jvmci-b01)
  • Oracle GraalVM 24.0.2+11.1 (build 24.0.2+11-jvmci-b01)
  • Oracle GraalVM 23.0.2+7.1 (build 23.0.2+7-jvmci-b01)
  • Oracle GraalVM 21.0.9+7.1 (build 21.0.9+7-LTS-jvmci-23.1-b79)

License

Copyright (C) 2022-2026 Takayuki Sato

This program is free software under MIT License.
See the file LICENSE in this distribution for more details.

About

A small framework to separate logics and data accesses for Java application.

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages