Describing services

Using our taxonomy to describe rich semantic services


A service is simply a group of operations.

service PeopleService {
   operation listAllPeople():Person[]


An operation defines a function on the API.

@HttpOperation(method = 'GET', url = 'https://myservice/people')
operation listAllPeople():Person[]

Operations often have annotations that provide hints to tooling as to how to invoke them.

Taxi ships with some standard annotations, as part of it's std-lib. Although it's up to individual tooling to determine how to interpret these, the suggested usage is as follows:

@HttpOperation(method,url)Indicates that the operation should be invoked over HTTP, using the provided method and url
@HttpRequestBodyIndicates that a parameter will be found on the request body
@ServiceDiscoveryClient(serviceName)Indicates that the service's absolute url should be discovered from Service Discovery, using the provided name for lookup
@PathVariable(name)Indicates a property from the path that should be populated with a variable passed to the operation


service MovieService {
   @HttpOperation(method = "GET" , url = "https://myMovices/movies")
   operation findAllMovies() : Movie[]

   @HttpOperation(method = "GET" , url = "https://myMovices/movies/{id}")
   operationFindMovie( @PathVariable(name = "id") id : MovieId ) : Movie

// Using a service discovery client, such as Eureka
@ServiceDiscoveryClient(serviceName = "actors")
service ActorService {
   @HttpOperation(method = "GET" , url = "/actors")
   operation findAllActors() : Actor[]

Names of operation parameters are optional. This is to encourage developers to leverage a richer type system where possible:

// These two declarations are both valid, and describe the same operation
operation convertUnits(source:Weight, targetUnit:Unit):Weight
operation convertUnits(Weight,Unit):Weight

Http Parameters

Operation Contracts & Constraints

Contracts and constraints are useful for telling tooling about what functionality an operation can provide, and what conditions must be met before invoking.

Both contracts and constraints use the same syntax.

type Money {
   currency : String
   amount : Decimal
operation convertCurrency(input: Money,
      targetCurrency: String) : Money(from input, currency = targetCurrency)

from input

A contract may indicate that a return type is derived from one of the inputs, by using the from {input} syntax:

operation convertUnits(input: Weight, target: Unit):Weight( from input )

Attribute constraints

Attribute constraints describe either a pre-condition (if on an input) or a post-condition (if on a return type) for an operation

operation convertFromPounds(input : Money(currency = 'GBP'), target: Currency)
    : Money( from input, currency = target)

As shown above, attribute constraints may either be:

  • A constant value (ie., "GBP")
  • A reference to an attribute of another parameter.
  • Nested syntax is supported (ie., foo.bar.baz)

These constraints are applicable on types too - see type constraints for an example.

This is an area of taxi that we expect to see significant development in, to improve the richness of the possible expressions

Database tables

Taxi supports declaring tables within a service using the table keyword:

service MyDatabase {
   table actors : Actor[]
   table movies : Movie[]

The return type of a table must be an array (either T[] or Array<T>) - a compilation error is thrown if a table declares a non-array type.

Taxi doesn't directly support specifying database connectivity details, or specifying the type of database. Use annotations to specify this level of information.

Streaming data sources

Streaming services (such as message brokers) can be declared using the stream keyword:

service MyMessageBroker {
   stream loggedOnEvents(): Stream<UserLoggedOnEvent>
   stream stockPrices(): Stream<StockPrice>

The return type of a stream must be an Stream<T> - a compilation error is thrown if a table declares a non-stream type.

Taxi doesn't directly support specifying message broker details, or specifying the message stream (eg., queue vs topic). Use annotations to specify this level of information.

Query operations

RESTful style APIs are great for discovering resources that have fairly predictable resource paths - such as /Author/jkrowling/Books, returns all the books that jkrowling has authored.

However, once you get beyond simple resource lookup, and into more bespoke query-style operations, services tend to expose their own query APIs. Taxi supports this with a rich, extensible syntax for exposing query contracts.

Here's an example:

taxiQl query personQuery(@RequestBody body:VyneQlQuery):Person[] with capabilities {

This describes:

  • A query endpoint that understands a query language called taxiQl
  • An operation named personQuery, which accepts a VyneQlQuery
  • The operation understands how to perform filtering using =, in and like semantics
  • And supports custom aggregation functions called sum and count.

Grammar names

In the above example, we saw a query endpoint that understands a query language called taxiQl.

From taxi's perspective, the name of the grammar being used is entirely arbitrary. The query server that is invoking endpoints must understand how to construct queries in the described language.

The Vyne query server understands how to construct taxiQl queries, with support for additional grammars (such as SQL, and GraphQL) planned. However, query servers may implement their own grammar support.

Streaming data

Data sources that expose a stream of data (such as a websocket, or a message queue) can expose this using the Stream<> type:

service PriceService {
   operation getPrices(): Stream<Price>

Documenting service lineage (experimental)

Services can publish lineage information describing indicating other operations they depend on, and the types of data they persist.

This can be useful input into data catalogs for building a graph of dependencies across an organisation.

Lineage can be declared in an optional lineage block within the service declaration:

service PersonService {
   lineage {
       consumes operation people.AddressService.resolvePostCodeToAddress
       stores Person
   operation findPerson(PersonId):Person

This indicates that our PersonService has an upstream dependency on another operation - resolvePostCodeToAddress, and additionally holds a persistent store of the Person model.

This data is exposed in the taxi schema and can be interrogated by tooling for integration with data catalogs.

Edit on GitLab