Wirespec showcase is all about showcasing interaction between services over network boundaries. We model the interaction between 4 services using Wirespec:
- Frontend: 🚀 A small application, Move-Money FE, that allows a user to visualize transactions and transfer money to others.
- Backend 1: 🔧 The Move-Money BE application, which handles incoming requests from the frontend but delegates transaction processing.
- Backend 2: 💳 The Payments BE application, responsible for performing all transactions.
- Audit Service: 📊 Listens to Kafka events to track everything that happens.
To get started, ensure the following tools are installed:
- 🐳 Docker and Docker Compose (tested with Docker v27)
- 🐳 Docker
- 🔨 Maven 3 (exact version specified in .mvnw properties)
- ☕ Java 21 (exact version in .sdkmanrc)
- 💻 IntelliJ IDEA (or your preferred IDE)
- 📦 Node.js (exact version in .nvmrc)
Run the following command to bring up the required services:
docker compose up --build -dWhich should result in 9 services starting in total, including a postgres db, kafka ecosystem, 3 spring services and a frontend app.
[+] Running 9/9
✔ Network wirespec-showcase_default Created 0.0s
✔ Container wirespec-showcase-zookeeper-1 Healthy 14.8s
✔ Container wirespec-showcase-postgresql-1 Healthy 20.1s
✔ Container wirespec-showcase-kafka-1 Healthy 19.4s
✔ Container wirespec-showcase-schema-registry-1 Healthy 29.9s
✔ Container wirespec-showcase-move-money-service-1 Healthy 59.5s
✔ Container wirespec-showcase-audit-service-1 Started 30.0s
✔ Container wirespec-showcase-payments-service-1 Started 30.1s
✔ Container wirespec-showcase-frontend-1 Started 59.6s
⏳ Note: The kafka ecosystem may take up to 30 seconds to start
To start exploring, open the frontend application at http://localhost:3000.
You'll find an interactive project overview page providing a quick introduction to the showcase. Dive in and explore the following features:
- Log in: Access the app using any of the provided user accounts (dummy functionality).
- View Transactions: Browse through transactions and inspect their states.
- Create Transactions: Transfer money to other users seamlessly.
- Audit Events: Monitor real-time audit events captured during your interactions within the application.
- Access the frontend: http://localhost:3000
- Explore Kafka Integration: http://localhost:9080
Below is a high-level overview of how the services interact in the Wirespec showcase project:
graph TD
subgraph Frontend
FE[Move-Money FE]
end
subgraph Backends
MM[Move-Money BE]
PS[Payments BE]
end
subgraph Audit
AUDIT[Audit Service]
end
DB[(PostgreSQL DB)]
KAFKA((Kafka Broker))
FE -->|REST API| MM
MM -->|REST API| PS
MM -->|Publishes Events| KAFKA
PS -->|Publishes Events| KAFKA
KAFKA -->|Kafka Events| AUDIT
AUDIT -->|Stores Audit Data| DB
- Frontend: The
Move-Money FEcommunicates with theMove-Money BEthrough REST APIs. - Move-Money Backend:
- Acts as the main API gateway between the frontend and the business logic.
- It interacts with the
Payments BEusing another REST API. - Publishes audit events to the
Kafka Broker.
- Payments Backend:
- Handles the actual processing of payments.
- Publishes its own audit events to the
Kafka Broker.
- Audit Service:
- Listens to audit events from the Kafka topic.
- Stores these events in the
PostgreSQL DBfor auditing purposes.
- Kafka Ecosystem: Serves as the central messaging hub for events generated by the backend services.
This diagram and description outline the key interactions within the system, showcasing the flow of data across services.
Let's dive into the highlights of Wirespec, starting with contract design.
At the core of wirespec lies contract design: what data do you want to exchange between two parties. As envisioned by Wirespec, a contract-first approach for interface design recognizes the importance of defining clear and detailed contracts (specifications) as the cornerstone of building robust interfaces.
The various contracts for wirespec showcase are designed and stored in the wirespec modules.
- move-money-api.ws - the contract for the REST api offered by the Move Money backend service. Used as a client by the frontend
- payments-api.ws - the contract for the REST api offered by the Payments service. Used as a client by the move money backend service
- audit-api.ws - the contract for the REST api offered by the Audit Service
- audit-events.ws - the contract for the Kafka integration for audit events. The audit service listens to a kafka topic on which such events are published. The move-money backend service and payment service both publish such audit events
Here is an example that showcases the different types and that are used during contract definition
/**
* A *refined type* representing a Currency symbol, either EUR of USD
*
* Refined types are essentially strings that conform to a regex pattern
*/
type Currency /^EUR|USD$/
/**
* A *refined type* representing a monetary value, optionally with exactly 2 digits, e.g. 1, 21.15, 0.70
*
* Refined types are essentially strings that conform to a regex pattern
*/
type MonetaryValue /^\d+(\.\d{2})?$/
/**
* A *complex type* representing a payment, with fields and type values
*/
type Payment {
/**
* A property *id* of primitive type String. Other primitives include Integer, Number, Boolean, Bytes
*/
id: String,
transactionCreatedTimestamp: Integer,
transactionCompletedTimestamp: Integer?,
senderAccountNumber: String,
senderAccountName: String,
recipientAccountNumber: String,
recipientAccountName: String,
amount: MonetaryValue,
currency: Currency
}
/**
* An *endpoint type* describing an HTTP endpoint to fetch payments
*
* Endpoints include:
* - name
* - http method
* - path
* - request body
* - query parameters
* - request headers
* - a list of all possible (unique) responses, each containing:
* - http statuscode
* - response body
* - response headers
*/
endpoint GetPayments GET /api/payments?{accountNumber: String, limit: Integer, offset: Integer } # { aRequestHeader: String } -> {
200 -> Payment[] # { page: Integer }
400 -> PaymentsApiError
500 -> PaymentsApiError
}
/**
* A *channel type* describing a model for passing Payments messages from one system to another
*
* Channels can be used to indicate massage-passing type of services, like Kafka, PubSub, and Queueing services where
* a server publishes messages on a channel, and a client consumes mesages from a channel.
*/
channel PaymentsChannel -> Payment
In addition to the contracts defined in Wirespec, the audit-related events are also defined using Avro schemas. Specifically, the schema audit_001.avsc defines the structure of the audit events that are published to Kafka by both the Move-Money Backend and Payments Services.
In addition to the contracts defined in Wirespec, the payments-api is also defined as an openapi specification.
The OpenAPI specification can be found at payments-openapi.json. This specification defines the RESTful API exposed by the Payments service, including details about endpoints, request/response structures, and required parameters.
Code generation enables a contract-first approach, fostering collaboration, creating reusable interfaces, and automating typesafe, dependency-free code from specifications.
To use the contracts defined earlier, we can generate source code in a language of our choice. Here's we'll use Kotlin
for our backends with the 🔗
wirespec-maven-plugin,
and Typescript for the frontend with the 🔗 @flock/wirespec npm
package
In order to generate server code for the endpoints defined
in move-money-api.ws, we make use the custom Spring emitter
offered by wirespec, see the move-money-server-api execution of the
wirespec-maven-plugin This does two things:
- Generate kotlin code from the wirespec file
- Annotates the various endpoint with an appropriate spring binding annotation:
@org.springframework.web.bind.annotation.GetMapping("<path/to/resource>")
Result: move-money-api.kt
Now we can implement a
restcontroller, TransactionController
in which we can focus on mapping the request to a response, as is defined in the contract. By implementing the various
Handler interfaces we can start mapping the incoming request to a response.
@RestController
class TransactionController :
GetMoveMoneyTransactionByIdEndpoint.Handler,
GetMoveMoneyTransactionsEndpoint.Handler,
SendMoneyEndpoint.Handler {
override suspend fun getMoveMoneyTransactions(request: GetMoveMoneyTransactionsEndpoint.Request): GetMoveMoneyTransactionsEndpoint.Response<*> {
TODO("...")
}
override suspend fun getMoveMoneyTransactionById(request: GetMoveMoneyTransactionByIdEndpoint.Request): GetMoveMoneyTransactionByIdEndpoint.Response<*> {
TODO("...")
}
override suspend fun sendMoney(request: SendMoneyEndpoint.Request): SendMoneyEndpoint.Response<*> {
TODO("...")
}
} We can then use the spring integration package to help bootstrap these endpoints and ensure serialisation and the mapping from the httpRequest and responses are mapped to the wirespec request and responses.
Adding the dependency
<dependency>
<groupId>community.flock.wirespec.integration</groupId>
<artifactId>spring-jvm</artifactId>
</dependency>And we have to add an @EnableWirespec annotation on the application
To consume REST endpoints as defined in Wirespec, while using Spring integration, we can use the default Kotlin emitter.
see the payments-client-api execution of the wirespec-maven-plugin
Result: payments-api.kt
Again, we can use the spring integration here, adding the dependency
<dependency>
<groupId>community.flock.wirespec.integration</groupId>
<artifactId>spring-jvm</artifactId>
</dependency>And we have to add an @EnableWirespec annotation on the application
To tell wirespec where the payments server lives, we have to configure a single prop:
wirespec.spring.webclient.base-url: http://localhost:8080Now, we can have an extremely lean PaymentsClient, which can use the autoconfigured WirespecWebClient as a proxy to
help map from a Request to a Response, without adding concerns on http traffic (unless told otherwise wirespec uses the
default WebClient configuration).
@Component
internal class PaymentsClient(
private val proxy: WirespecWebClient,
) {
suspend fun getPayments(req: GetPaymentsEndpoint.Request): GetPaymentsEndpoint.Response<*> = proxy.send(req)
suspend fun getPayment(req: GetPaymentByIdEndpoint.Request): GetPaymentByIdEndpoint.Response<*> = proxy.send(req)
suspend fun postPayment(req: PostPaymentEndpoint.Request): PostPaymentEndpoint.Response<*> = proxy.send(req)
}It is also possible to add the bootstrapping to enable Kafka messaging based on the channel type in Wirespec.
See the audit-api-avro-schema execution of the wirespec-maven-plugin
Result: audit_001.kt
Leaving aside the configuration of Kafka, and the topic selection, the implementation of the AuditEventRecordChannel
is simply:
@Component
internal class AuditKafkaClient(
kafkaProducerFactory: ProducerFactory<Any, Any>,
) : AuditEventRecordChannel {
private val template = KafkaTemplate(kafkaProducerFactory)
override fun invoke(message: AuditEventRecord) {
val avro = AuditEventRecord.Avro.to(message)
template.send("audit-messages", avro)
}
}Integration with Wirespec in a TypeScript project is streamlined using the TypeScript emitter provided by the Wirespec toolchain. The emitter generates client-side code that can be used to consume REST endpoints defined by Wirespec contracts. This eliminates the need to manually write and maintain request/response handling logic. By leveraging this integration, developers can focus on implementing business logic while relying on the generated code to handle the serialization, deserialization, and HTTP-related concerns seamlessly. The generated client code can easily integrate with popular HTTP libraries, such as Axios or Fetch API, ensuring flexibility and compatibility with existing stacks. Additionally, these TypeScript definitions provide type safety, helping catch potential errors during development.
To generate Typescript code for the move-money-api we use the cli from the npm package:
wirespec compile -i ../../wirespec/src/main/resources/wirespec/move-money-api.ws -o ./src/main/react -l TypeScript -p 'src.wirespec'Result Move-money-api.ts
export const fetchTransactions: () => Promise<Transaction[]> = async () => {
const response = await transactionClient.getMoveMoneyTransactions(
GetMoveMoneyTransactions.request({
token: USER_TOKEN,
xforwardedfor: CLIENT_IP,
limit: 10,
offset: 0,
}),
);
switch (response.status) {
case 200: {
return consumeMoveMoneyTransactions(response);
}
case 400:
case 403:
case 500:
throw new ShowcaseError(
"Server error occurred while fetching transactions",
response.body,
);
}
};
const consumeMoveMoneyTransactions: (response: GetMoveMoneyTransactions.Response200,) => Transaction[] =
(response) => {
/*...*/
};Together with a small mapping from the wirespec request to the wirespec response, using the generated to and from
method of wirespec.
// Transactionclient.ts
const getMoveMoneyTransactions: (
request: GetMoveMoneyTransactions.Request,
) => Promise<GetMoveMoneyTransactions.Response> = async (request) => {
const rawRequest = this.getMoveMoneyTransactionsClient.to(request);
const rawResponse = await wirespecWebClient(rawRequest);
return this.getMoveMoneyTransactionsClient.from(rawResponse);
};And a generic fectch webclient being able to map any RawRequest to a RawResponse, see
WirespecWebClient.ts, which is a onetime implementation
you can reuse for any endpoint defined by wirespec.
Another possibility is to easily convert existing specifications into Wirespec contracts. This facilitates seamless integration and adoption of Wirespec by leveraging already-defined APIs or schemas from other systems. Below are two prominent conversion examples:
The conversion from Avro schema to Wirespec is handled using the wirespec-maven-plugin. Here's how it works:
<execution>
<id>convert-avro</id>
<goals><goal>convert</goal></goals>
<configuration>
<input>src/main/resources/avro/audit_001.avsc</input>
<output>${project.build.directory}/generated-resources</output>
<packageName>wirespec</packageName>
<languages>
<language>Wirespec</language>
</languages>
<format>Avro</format>
</configuration>
</execution>Key components:
- The plugin takes an Avro schema file (
.avsc) as input - Specifies the output directory for the generated Wirespec file
- Sets the target language as "Wirespec"
- Uses the "Avro" format for conversion
ℹ️ Besides converting from Avro to Wirespec, it is also possible to generate Kotlin code directly from an avro spec.
Similarly, OpenAPI specifications can be converted to Wirespec:
<execution>
<id>convert-openapi</id>
<goals><goal>convert</goal></goals>
<configuration>
<input>src/main/resources/openapi/payments-openapi.json</input>
<output>${project.build.directory}/generated-resources</output>
<packageName>wirespec</packageName>
<languages>
<language>Wirespec</language>
</languages>
<format>OpenAPIV3</format>
</configuration>
</execution>Key components:
- Takes an OpenAPI specification file (
.json) as input - Generates Wirespec files in the specified output directory
- Sets the target language as "Wirespec"
- Uses the "OpenAPIV3" format for conversion
ℹ️ Besides converting from OpenApi v2 or v3 to Wirespec, it is also possible to generate Kotlin code directly from an openapi spec.
Validation is ...
Coming soon
