Introduction
Why Pyranid?
Pyranid makes working with JDBC pleasant and puts your relational database first. Writing SQL "just works". Closure-based transactions are simple to reason about. Modern Java language features are supported.
No new query languages to learn. No leaky object-relational abstractions. No kitchen-sink frameworks that come along for the ride. No magic.
Pyranid is commercially-friendly Open Source Software, proudly powering production systems since 2015.
Design Goals
- Small codebase
- Customizable
- Threadsafe
- Zero dependencies
- DI-friendly
Pyranid is built to be small and easy to understand.
Its API design aims for a minimal footprint with a high strength-to-weight ratio.
Do Zero-Dependency Libraries Interest You?
Similarly-flavored commercially-friendly OSS libraries are available.
- Soklet is a DI-friendly HTTP 1.1 server that supports Virtual Threads
- Lokalized enables natural-sounding translations (i18n) via an expression language
Installation
Pyranid is a single JAR, available on Maven Central.
Maven
Java 17+
<dependency>
  <groupId>com.pyranid</groupId>
  <artifactId>pyranid</artifactId>
  <version>3.0.0</version>
</dependency>
<dependency>
  <groupId>com.pyranid</groupId>
  <artifactId>pyranid</artifactId>
  <version>3.0.0</version>
</dependency>
Java 8+ (legacy; only critical fixes will be applied)
<dependency>
  <groupId>com.pyranid</groupId>
  <artifactId>pyranid</artifactId>
  <version>1.0.17</version>
</dependency>
<dependency>
  <groupId>com.pyranid</groupId>
  <artifactId>pyranid</artifactId>
  <version>1.0.17</version>
</dependency>
Gradle
repositories {
  mavenCentral()
}
dependencies {
  implementation 'com.pyranid:pyranid:3.0.0'
}
repositories {
  mavenCentral()
}
dependencies {
  implementation 'com.pyranid:pyranid:3.0.0'
}
Direct Download
If you don't use Maven, you can drop pyranid-3.0.0.jar directly into your project. No other dependencies are required.
Example Usage
First, obtain a javax.sql.DataSource and use it to back your Database.
// Use any javax.sql.DataSource you like
DataSource dataSource = new HikariDataSource(new HikariConfig() {{
  setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
  setUsername("example");
  setPassword("secret");
  setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// Initialize with default configuration.
// These instances are threadsafe and intended to be shared across your app
Database database = Database.withDataSource(dataSource).build();
// Use any javax.sql.DataSource you like
DataSource dataSource = new HikariDataSource(new HikariConfig() {{
  setJdbcUrl("jdbc:postgresql://localhost:5432/my-database");
  setUsername("example");
  setPassword("secret");
  setConnectionInitSql("SET TIME ZONE 'UTC'");
}});
// Initialize with default configuration.
// These instances are threadsafe and intended to be shared across your app
Database database = Database.withDataSource(dataSource).build();
Then, define some types...
enum DepartmentId {
  ACCOUNTING,
  HR
}
record Employee (
  UUID employeeId,
  DepartmentId departmentId,
  String name,
  BigDecimal salary,
  ZoneId timeZone,
  Locale locale,
  Instant createdAt
) {}
enum DepartmentId {
  ACCOUNTING,
  HR
}
record Employee (
  UUID employeeId,
  DepartmentId departmentId,
  String name,
  BigDecimal salary,
  ZoneId timeZone,
  Locale locale,
  Instant createdAt
) {}
...and do some work:
void awardAnnualRaises(DepartmentId departmentId) {
  payrollSystem.startLengthyWarmupProcess();
  // Ensure this set of operations commits or rolls back atomically.
  // A rollback occurs if an exception bubbles out
  database.transaction(() -> {
    // Pull a list of all employees in the department
    List<Employee> employees = database.queryForList("""
      SELECT *
      FROM employee
      WHERE department_id=?
      """, Employee.class, departmentId);
    // Get a reference to the transaction that's scoped to this closure
    Transaction transaction = database.currentTransaction().get();      
    // Calculate and apply a raise for everyone
    for(Employee employee : employees) {
      BigDecimal newSalary = payrollSystem.salaryWithAnnualRaise(employee));
      // Make a savepoint we can roll back to if something goes wrong
      Savepoint savepoint = transaction.createSavepoint();
      try {
        database.execute("""
          UPDATE employee
          SET salary=?
          WHERE employee_id=?
          """, newSalary, employee.employeeId());
      } catch(DatabaseException e) {
        // Detect a constraint violation and gracefully continue on
        if("salary_too_big".equals(e.getConstraint().orElse(null)) {
          // Put transaction back in good state
          // (prior to constraint violation)
          transaction.rollback(savepoint);
          
          out.printf("Salary %s is too big for employee %s\n", 
            newSalary, employee.employeeId());
        } else {      
          // There must have been some other problem, bubble out
          throw e;
        }        
      }
    }
    // Schedule some work to be done after this transaction ends
    transaction.addPostTransactionOperation((transactionResult) -> {
      if(transactionResult == TransactionResult.COMMITTED) {
        // Successful commit?
        // Email everyone with the good news
        for(Employee employee : employees)
          sendCongratulationsEmail(employee);
      } else if(transactionResult == TransactionResult.ROLLED_BACK) {
        // Exception bubbled out?
        // Do some additional cleanup
        payrollSystem.cancelLengthyWarmupProcess();	
      }
    });   
  });
}
void awardAnnualRaises(DepartmentId departmentId) {
  payrollSystem.startLengthyWarmupProcess();
  // Ensure this set of operations commits or rolls back atomically.
  // A rollback occurs if an exception bubbles out
  database.transaction(() -> {
    // Pull a list of all employees in the department
    List<Employee> employees = database.queryForList("""
      SELECT *
      FROM employee
      WHERE department_id=?
      """, Employee.class, departmentId);
    // Get a reference to the transaction that's scoped to this closure
    Transaction transaction = database.currentTransaction().get();      
    // Calculate and apply a raise for everyone
    for(Employee employee : employees) {
      BigDecimal newSalary = payrollSystem.salaryWithAnnualRaise(employee));
      // Make a savepoint we can roll back to if something goes wrong
      Savepoint savepoint = transaction.createSavepoint();
      try {
        database.execute("""
          UPDATE employee
          SET salary=?
          WHERE employee_id=?
          """, newSalary, employee.employeeId());
      } catch(DatabaseException e) {
        // Detect a constraint violation and gracefully continue on
        if("salary_too_big".equals(e.getConstraint().orElse(null)) {
          // Put transaction back in good state
          // (prior to constraint violation)
          transaction.rollback(savepoint);
          
          out.printf("Salary %s is too big for employee %s\n", 
            newSalary, employee.employeeId());
        } else {      
          // There must have been some other problem, bubble out
          throw e;
        }        
      }
    }
    // Schedule some work to be done after this transaction ends
    transaction.addPostTransactionOperation((transactionResult) -> {
      if(transactionResult == TransactionResult.COMMITTED) {
        // Successful commit?
        // Email everyone with the good news
        for(Employee employee : employees)
          sendCongratulationsEmail(employee);
      } else if(transactionResult == TransactionResult.ROLLED_BACK) {
        // Exception bubbled out?
        // Do some additional cleanup
        payrollSystem.cancelLengthyWarmupProcess();	
      }
    });   
  });
}
Want to see what else you can do? Start with Configuration.



