SDKs

Java / Kotlin SDK

The taxiql-jvm library lets you execute TaxiQL queries from Java and Kotlin applications, using query classes generated from your .taxi files at build time.

Available since 1.70.0

​
Installation

All modules are published to Maven Central under the org.taxilang group. Add the core module plus whichever transport suits your stack.

​
Maven

<!-- Core interfaces (required) -->
<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-jvm-core</artifactId>
  <version>VERSION</version>
</dependency>

<!-- Pick one transport -->
<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-ktor</artifactId>
  <version>VERSION</version>
</dependency>

​
Gradle

implementation("org.taxilang:taxiql-jvm-core:VERSION")
implementation("org.taxilang:taxiql-transport-ktor:VERSION")

Replace VERSION with the latest release:

Releases are published to Maven Central.

Snapshot versions are published to the Orbital snapshot repository. To use a snapshot, add the repository to your pom.xml:

<repositories>
  <repository>
    <id>orbital-snapshots</id>
    <url>https://repo.orbitalhq.com/snapshot</url>
    <snapshots>
      <enabled>true</enabled>
    </snapshots>
  </repository>
</repositories>

​
Quick start

Given these queries in a .taxi file:

namespace com.acme.flights

query FindFlights {
    find { Flight[] } as {
        id : FlightNumber
        departs : Origin
        arrives : Destination
    }[]
}

query FindFlightDetails(flightNumber: FlightNumber) {
    find { Flight(FlightNumber == flightNumber) } as {
        id : FlightNumber
        departs : Origin
        arrives : Destination
    }
}

The taxiql-maven-plugin generates FindFlightsQuery and FindFlightDetailsQuery at build time. You can use them directly:

import com.acme.flights.FindFlightDetailsQuery
import com.acme.flights.FindFlightsQuery
import lang.taxilang.jvm.transport.ktor.KtorTaxiTransport

suspend fun main() {
    val client = KtorTaxiTransport("https://orbital.example.com")

    // Fetch all flights
    val flights = client.executeForResult(FindFlightsQuery()).getOrThrow()
    println("Found ${flights.size} flights")

    // Stream flights one by one
    client.flow(FindFlightsQuery())
        .onEach { println("${it.id}: ${it.departs} → ${it.arrives}") }
        .collect()

    // Fetch a single flight by number
    val flight = client.executeForResult(FindFlightDetailsQuery("QF-1")).getOrThrow()
    println("${flight.departs} → ${flight.arrives}")
}

​
Code generation

​
Maven plugin

Add taxiql-maven-plugin to your pom.xml. It runs automatically during generate-sources and registers the output as a compile source root, so the generated classes are available to your code without any extra steps.

<plugin>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-maven-plugin</artifactId>
  <version>VERSION</version>
  <executions>
    <execution>
      <goals>
        <goal>generate-sources</goal>
      </goals>
    </execution>
  </executions>
</plugin>

​
Project structure

The plugin reads your taxi.conf to find query files. By default it looks for the conf file at src/main/taxi/taxi.conf:

# taxi.conf
name: com.acme/flights
version: 0.1.0
sourceRoot: src/

Named queries anywhere under sourceRoot are picked up and compiled. A common layout:

src/main/java          ← java source root
src/main/taxi          ← taxi source root
  src/
    schema.taxi          ← type definitions
    queries/
      FindFlights.taxi   ← named queries
      FindFlightDetails.taxi
  taxi.conf

Running mvn generate-sources (or any lifecycle phase that includes it - like mvn compile) writes a Java class per query to target/generated-sources/taxi/.

​
Loading types from a remote Orbital server

If your types are defined in a running Orbital instance rather than local .taxi files, point the plugin at the server and it will pull the schema automatically:

<configuration>
  <remoteUrl>https://orbital.example.com</remoteUrl>
  <!-- optional - provide authentication headers -->
  <remoteAuthHeaderValue>Bearer ${orbital.token}</remoteAuthHeaderValue>
</configuration>

The remote schema is used to resolve types referenced in your queries. The generated classes still come from the queries in your local source tree.

​
What gets generated

For each named query, the plugin produces a Java class that:

  • implements UnaryTaxiQlQuery<List<T>> and StreamableTaxiQlQuery<T> for find { T[] } queries, so you can either fetch the full list at once or stream results one by one with the same class
  • implements UnaryTaxiQlQuery<T> for find { T } queries
  • implements StreamableTaxiQlQuery<T> for stream { T } queries
  • has a typed constructor for each query parameter

See reference docs below for examples.

​
Gradle plugin

A Gradle plugin is planned. If you’d like to use one, please reach out on Slack.


​
Transports

​
Ktor

Best choice for Kotlin applications. KtorTaxiTransport uses coroutines throughout and exposes executeForResult() and flow() as the primary API.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-ktor</artifactId>
  <version>VERSION</version>
</dependency>
val client = KtorTaxiTransport(
    baseUrl = "https://orbital.example.com",
    authHeaderProvider = { mapOf("Authorization" to "Bearer ${token()}") }
)

KtorTaxiTransport implements AutoCloseable — use it in a use block or inject it as a long-lived singleton and call close() on shutdown.

​
Fetching a single result

val result = client.executeForResult(FindFlightsQuery())

result.onSuccess { flights -> println("Found ${flights.size} flights") }
result.onFailure { error -> println("Failed: ${error.message}") }

executeForResult() is a suspend function that returns Result<T>. It surfaces server-side failures without throwing, which makes it composable in coroutine pipelines.

​
Streaming results

client.flow(FindFlightsQuery())
    .onEach { println("${it.id}: ${it.departs} → ${it.arrives}") }
    .collect()

flow() returns a Kotlin Flow<T> where T is the element type. Errors during the stream (including server-reported errors) are thrown as exceptions and can be caught in the normal coroutine way.


​
Spring WebClient

Best choice for Spring WebFlux applications. stream() returns a Flux<T> and mono() returns a Mono<T>, making it a natural fit for reactive pipelines.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-webclient</artifactId>
  <version>VERSION</version>
</dependency>

Create a WebClient with the server base URL and pass it to the transport:

@Bean
fun taxiTransport(): WebClientTaxiTransport {
    val webClient = WebClient.builder()
        .baseUrl("https://orbital.example.com")
        .defaultHeader("Authorization", "Bearer ${token()}")
        .build()
    return WebClientTaxiTransport(webClient)
}

​
Fetching a single result

fun findFlights(): Mono<List<FindFlightsResult>> =
    transport.mono(FindFlightsQuery())

fun findFlightDetails(number: String): Mono<FindFlightDetailsResult> =
    transport.mono(FindFlightDetailsQuery(number))

​
Streaming results

fun streamFlights(): Flux<FindFlightsResult> =
    transport.stream(FindFlightsQuery())

Errors during the stream surface as Flux.error / Mono.error and can be handled with .onErrorMap() or .doOnError().


​
Java HttpClient

Zero additional dependencies — uses the java.net.http.HttpClient built into Java 11+. Best for Java applications or anywhere you want to minimise the dependency footprint.

<dependency>
  <groupId>org.taxilang</groupId>
  <artifactId>taxiql-transport-http-client</artifactId>
  <version>VERSION</version>
</dependency>
var transport = new JavaHttpClientTaxiTransport(
    "https://orbital.example.com",
    () -> Map.of("Authorization", "Bearer " + token())
);

​
Fetching a single result

// Non-blocking
transport.execute(new FindFlightsQuery())
    .thenAccept(flights ->
        System.out.println("Found " + flights.size() + " flights")
    );

// Blocking
var flights = transport.execute(new FindFlightsQuery()).get();

​
Streaming results

stream() returns a Publisher<T> compatible with Project Reactor, RxJava, and any other Reactive Streams library:

transport.stream(new FindFlightsQuery())
    .subscribe(new Subscriber<>() {
        public void onSubscribe(Subscription s) { s.request(Long.MAX_VALUE); }
        public void onNext(FindFlightsResult f) { System.out.println(f.getId()); }
        public void onError(Throwable t) { t.printStackTrace(); }
        public void onComplete() { System.out.println("Done"); }
    });

​
Parameterized queries

When a query declares parameters, the generated class has a constructor that accepts them:

query FindFlightDetails(flightNumber: FlightNumber) {
    find { Flight(FlightNumber == flightNumber) } as { ... }
}
// Kotlin
val flight = client.executeForResult(FindFlightDetailsQuery("QF-1")).getOrThrow()
// Java
var flight = transport.execute(new FindFlightDetailsQuery("QF-1")).get();

Parameter types follow the Taxi type system. For type aliases wrapping primitives (like FlightNumber inherits String), the constructor parameter is the underlying Java type (String in this case), annotated with @DataType carrying the Taxi type name. This lets frameworks that understand Taxi semantics do type-safe wiring automatically.


​
Error handling

​
Query failures

When the server rejects a query — either with a non-2xx HTTP response or by emitting an event:error SSE event mid-stream — the SDK surfaces a QueryFailureException containing the server’s error message.

With executeForResult() (Ktor), failures come back as Result.failure rather than exceptions:

val result = client.executeForResult(FindFlightsQuery())
result
    .onSuccess { flights -> println("Found ${flights.size} flights") }
    .onFailure { error ->
        if (error is QueryFailureException)
            println("Query failed: ${error.failure.message}")
    }

With Spring WebClient, they surface as Mono.error / Flux.error:

transport.mono(FindFlightsQuery())
    .onErrorMap(QueryFailureException::class.java) { e ->
        MyApplicationException("Query failed: ${e.failure.message}")
    }

With CompletableFuture (Ktor’s execute() or Java HttpClient), the exception is wrapped in a standard ExecutionException:

try {
    val flights = transport.execute(FindFlightsQuery()).get()
} catch (e: ExecutionException) {
    val cause = e.cause
    if (cause is QueryFailureException)
        println("Query failed: ${cause.failure.message}")
}

​
SSE stream errors

The server can signal an error mid-stream using an SSE error event:

event:error
data:{"message":"No data sources were found that can return Flight[]", ...}

The SDK detects this and terminates the stream with a QueryFailureException, using whatever error channel is natural for the transport. Any results emitted before the error are delivered normally.

With Ktor flow(), catch it like any other exception:

try {
    client.flow(FindFlightsQuery())
        .onEach { println(it.id) }
        .collect()
} catch (e: QueryFailureException) {
    println("Stream failed: ${e.failure.message}")
}

With Spring WebClient’s Flux, handle it with .doOnError():

transport.stream(FindFlightsQuery())
    .doOnError(QueryFailureException::class.java) { e ->
        println("Stream failed: ${e.failure.message}")
    }
    .subscribe()

Jackson configuration

All transports use Jackson for JSON deserialization. The default ObjectMapper is Kotlin-aware (via jackson-module-kotlin). Pass a custom objectMapper to the constructor if you need specific serialization settings such as custom date formats or naming strategies.


​
Reference

The following classes are used by the SDK. They’re generated by code-gen tools, such as the Maven plugin, rather than intended to be written by hand.

​
TaxiTransport

The core interface implemented by all three transports:

interface TaxiTransport {
    fun <T> stream(query: StreamableTaxiQlQuery<T>): Publisher<T>
    fun <T> execute(query: UnaryTaxiQlQuery<T>): CompletableFuture<T>
}

KtorTaxiTransport and WebClientTaxiTransport extend this with idiomatic extras (flow(), executeForResult(), mono()) — prefer those in Kotlin and Spring code respectively.

​
UnaryTaxiQlQuery

Used for queries that return a single result or a complete list in one response:

// Single object
class FindFlightQuery(private val flightId: String) : UnaryTaxiQlQuery<Flight> {
    override val taxiQl = """
        query FindFlight(flightId: FlightId) {
            find { Flight( FlightId == flightId ) }
        }
    """.trimIndent()
    override val arguments = mapOf("flightId" to flightId)
    override val responseType = object : TypeReference<Flight>() {}
}

// Collection
class FindAllFlightsQuery : UnaryTaxiQlQuery<List<Flight>> {
    override val taxiQl = "find { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val responseType = object : TypeReference<List<Flight>>() {}
}

​
StreamableTaxiQlQuery

Used for queries that stream results one element at a time:

class LiveFlightsQuery : StreamableTaxiQlQuery<Flight> {
    override val taxiQl = "stream { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val streamType = object : TypeReference<Flight>() {}
}

A find { T[] } query can implement both interfaces on the same class, allowing the caller to choose between receiving the full list at once or streaming element by element:

class FindAllFlightsQuery : UnaryTaxiQlQuery<List<Flight>>, StreamableTaxiQlQuery<Flight> {
    override val taxiQl = "find { Flight[] }"
    override val arguments = emptyMap<String, Any?>()
    override val responseType = object : TypeReference<List<Flight>>() {}
    override val streamType = object : TypeReference<Flight>() {}
}

The taxiql-maven-plugin generates this dual-interface pattern automatically for all find { T[] } queries.

Previous
Linter
Next
Adopting semantic types