How to Implement the Pipes and Filters Architecture with Java and Azure

In my previous blog post, I provided an in-depth explanation of the Pipes and Filters architecture, which you can check out here. To recap, the Pipes and Filters architecture breaks down a system into small, self-contained processing components known as filters. Each filter is responsible for performing a specific task or transformation on the data it receives, promoting modularity and reusability. These filters are connected via pipes, which facilitate the flow of data from one filter to the next. This architecture is particularly effective in scenarios involving data integration, processing workflows, transformation pipelines, and stream processing.

In this blog post, I will walk you through a sample implementation of the Pipes and Filters architecture using Java and Azure. Our example project will centre around a chatbot designed to assist in creative thinking activities such as brainstorming.

Sample Project Overview

The goal of this project is to create a tool that integrates with a company’s creative thinking solutions. Specifically, it’s a chatbot that aids teams during brainstorming sessions and other creative activities. The process begins when a user interacts with the application by typing a question, such as “How will the ongoing AI revolution affect the financial industry?” This question is then sent to the application for processing.

How the System Works

  1. Input Validation: The first filter is responsible for validating the user’s question. The question might be in a language that the AI model doesn’t understand, or it might be too long or contain sensitive information. Therefore, the first task is to verify whether the question can be processed further.
  2. Prompt Engineering: If the question is valid, the application uses predefined templates to enrich it. These templates provide context to the AI-powered tool, making the model’s output more valuable. For example, a template might be: “You are a CEO. Given a strategic prompt, you will create X futuristic, hypothetical scenarios that happen Y years from now. The strategic prompt is: Z”. This step is crucial as it leverages prompt engineering to guide the AI model in generating more meaningful responses.
  3. AI Model Interaction: The final step involves sending the enriched prompts to the AI model, which processes them and generates answers. These answers are then displayed back to the user.

Implementation Details

The system consists of three filters:

  1. Input Validation Filter: Validates the user’s input according to the application’s data requirements.
  2. Prompt Engineering Filter: Analyses and enriches the validated input to create a prompt.
  3. AI Model Facade Filter: Sends the engineered prompt to the AI model and handles the response.

The First Filter: Input Validation

The first filter is implemented as an Azure Function, and its primary role is to validate the incoming question.

@FunctionName("QuestionValidationFunction")
public HttpResponseMessage validate(
       @HttpTrigger(name = "question",
               methods = {HttpMethod.POST},
               authLevel = AuthorizationLevel.FUNCTION)
       HttpRequestMessage<String> question,
       @QueueOutput(name = "questionQueue",
               queueName = "question-queue",
               connection = "AzureWebJobsStorage")
       OutputBinding<String> questionQueue,
       ExecutionContext executionContext) {
    // Implementation of validation.
}

The validate method, annotated with @FunctionName("QuestionValidationFunction"), is triggered by an HTTP request. It takes two parameters: the HTTP request containing the question and an output binding to a storage queue named "question-queue". The method validates the question and, if valid, sends it down the pipeline.

The Second Filter: Prompt Engineering

The second filter enriches the validated question with a template to maximise the AI model’s response quality.

@FunctionName("PromptEngineeringFunction")
public void sendPrompt(
       @QueueTrigger(
               name = "question",
               queueName = "question-queue",
               connection = "AzureWebJobsStorage")
       String question,
       @QueueOutput(
               name = "promptQueue",
               queueName = "prompt-queue",
               connection = "AzureWebJobsStorage")
       OutputBinding<String> promptQueue,
       ExecutionContext executionContext) {
   // Prompt engineering logic.
}

This function is triggered by messages in the "question-queue". When a new message arrives, the function is invoked, and the question is enriched before being sent to the next queue, "prompt-queue".

The Third Filter: AI Model Facade

The third filter handles communication with the AI model. This filter is implemented using the Spring Cloud Function framework, which decouples infrastructure configuration from the business logic. I’ll describe it in detail in the next blog post, but I’ll give you a short description here so you understand the code.

The functions are implemented as Java function interfaces and autowired into respective request handlers. The handlers contain logic that configures integration with the serverless platform provider. In our case it’ll be the Azure SDK (which examples we’ve seen before). With this setup, you can change the cloud provider by simply rewriting the handlers (and changing build definition) without any need to rewrite the functions itself. 

Let’s now look at the function’s code. 

@Bean
public Function<String, String> answer(ModelClient modelClient) {
   	// Function’s logic
}

The answer function is a simple Java function interface that handles the logic for interacting with the AI model. It is autowired into a handler that manages the integration with Azure.

@Component
public class AnswerHandler {

   private final Function<String, String> answer;

   public AnswerHandler(Function<String, String> answer) {
       this.answer = answer;
   }

   @FunctionName("answer")
   public void answer(
           @QueueTrigger(
                   name = "promptQueue",
                   queueName = "prompt-queue",
                   connection = "AzureWebJobsStorage")
           String prompt,
           @QueueOutput(
                   name = "answerQueue",
                   queueName = "answer-queue",
                   connection = "AzureWebJobsStorage")
           OutputBinding<String> answerQueue
   ) {
       // Handler’s logic
   }
}

This handler is similar to the previous filters, but it delegates the business logic to the answer function. The answerQueue is used to send the final answer for further consumption. 

Deployment

With all three filters implemented, you can now deploy the application to Azure, to play with the code. The deployment process can be accomplished using Maven, as described in this article

In summary, we implemented a complete Pipes and Filters architecture using both the Azure SDK and Spring Cloud Function. The system comprises three filters – each responsible for a distinct part of the application’s workflow: input validation, prompt engineering, and AI model communication. The unidirectional data flow is managed primarily by queues, ensuring a clean separation of concerns and easy scalability.

Summary

This blog post demonstrates how to implement the Pipes and Filters architecture using Java and Azure for a chatbot that assists in creative thinking activities. The architecture is broken down into three filters: input validation, prompt engineering, and AI model interaction. Each filter handles a specific task in the data processing pipeline, ensuring modularity and reusability. The post also covers the deployment process using Azure and Spring Cloud Function, highlighting the benefits of separating business logic from infrastructure configuration.

If you’re interested in how this architecture style can be used to implement serverless solutions, and how to work with Azure Functions in Java, check out my Udemy course that covers these topics in detail.  

The working code example can be found on my GitHub

Serverless with the Pipes and Filters Architecture

In a previous blog post I argued that serverless computing is not an architecture style. However, I made a point there that – as with every technology – some architecture styles go along with it better than the others. What most probably comes to your mind are microservices and event-driven architecture. In this blog post though, I would like to cover a less known architecture style that seems to perfectly match serverless computing – the pipes and filters architecture, which emphasises modularity and reusability in processing components. This architecture is built on the concept of “pipes” that connect filters, enabling the transformation or processing of data as it flows through the system. 

Understanding Pipes and Filters Architecture

The Pipes and Filters architecture is characterised by decomposing a system into small, self-contained processing components called filters. Each filter performs a specific task or transformation on the data it receives, promoting a highly modular and reusable design. Filters are connected using pipes, which serve as conduits for data flow, ensuring a one-way flow of information through the system. This architecture is particularly useful in scenarios such as data integration, data processing workflows, data transformation pipelines, and stream processing systems.

Key features of the Pipes and Filters architecture include:

  • Modularity and Reusability: Each filter is designed to be independent, reusable, and replaceable. This modularity allows for flexible composition of processing components.
  • Sequential Data Flow: Data passes from one filter to the next through pipes, enabling sequential processing.
  • Loose Coupling: Filters interact through well-defined data interfaces provided by the pipes, promoting loose coupling and reusability.
  • Scalability and Parallelism: Filters can be replicated or distributed to handle increased processing loads, allowing for parallel processing.
  • Flexibility and Adaptability: Filters can be added, removed, or rearranged within the pipeline to accommodate changing processing needs.

Serverless Pipes and Filters

Serverless computing naturally complements the Pipes and Filters architecture through several key features. One significant aspect is the modularity and reusability inherent in both paradigms. In serverless computing, individual functions are designed to perform specific tasks and can be independently deployed, updated, or replaced without impacting the rest of the system. This mirrors the independent and reusable nature of filters in the Pipes and Filters architecture, where each filter is a standalone processing unit. Additionally, serverless platforms inherently support sequential data flow through event-driven triggers, similar to how data flows through pipes from one filter to another. This ensures that each function or filter performs its task in sequence, enhancing the clarity and manageability of the data processing pipeline. Moreover, serverless functions communicate through well-defined event interfaces, which aligns with the loose coupling seen in the Pipes and Filters architecture. This separation allows for easier maintenance and testing, as changes in one function or filter do not directly affect others. The scalability and parallelism provided by serverless architectures are also a perfect match for the scalable and distributable nature of filters, allowing the system to handle varying loads efficiently. Finally, the flexibility and adaptability of serverless functions, which can be quickly modified or scaled, resonate with the ability to add, remove, or rearrange filters within the pipeline, making it easy to adapt to changing requirements or workloads.

Functions Chaining

There is one special feature of serverless computing that goes particularly well with the pipes and filters architecture – functions chaining. It is the practice of linking multiple serverless functions together, where the output of one function serves as the input for the next. This allows for the creation of complex workflows by decomposing tasks into smaller, manageable, and reusable functions that execute sequentially. It mirrors exactly how a pipes-and-filters application should be designed, and that is the main reason why serverless computing is my go-to technology whenever I use the pipes and filter architecture in my design.

Summary

In this blog post, we explored the synergy between serverless computing and the pipes and filters architecture, a lesser-known but highly effective design that emphasises modularity and reusability in processing components. The pipes and filters architecture decomposes systems into self-contained filters connected by pipes, ensuring a sequential data flow. Serverless computing complements this by offering modular, independently deployable functions that support sequential processing and loose coupling through well-defined event interfaces. This combination enhances scalability, parallelism, and flexibility, making it ideal for dynamic workloads. A key feature of serverless computing, function chaining, perfectly aligns with the pipes and filters model, enabling the creation of complex, manageable workflows.

In the next blog post I will show you in example how serverless computing can be used in such a design.

Understanding Serverless Computing. Not Just an Architecture Style

Understanding IT architecture styles is crucial for anyone involved in the design and development of software systems. These styles provide a framework of principles, patterns, and guidelines that shape the structure, organisation, and behaviour of IT applications. However, there is often confusion between architectural styles and deployment models, with serverless computing frequently misinterpreted as an architecture style. It is quite common to include serverless as a new architecture style, along microservices or event-driven architecture. In this article I aim to clarify this misconception, emphasising that serverless is primarily a deployment model rather than an architectural style. By exploring the comprehensive role of architectural styles and the distinct nature of serverless computing, I will highlight why understanding this distinction is essential for effectively leveraging serverless in your IT solutions.

What is an IT Architecture Style?

An IT architecture style is a comprehensive set of principles, patterns, and guidelines that define the overall structure, organisation, and behaviour of an information technology system or application. It provides a high-level framework for designing, developing, and deploying IT solutions. Regardless of the specific architectural style you apply, they all share several common features.

  • Principles and Concepts. A software architecture style defines a set of principles, concepts, and practices that guide the design of software systems. These principles and concepts provide a common vocabulary and shared understanding among software architects.
  • Structural Elements. It defines the structural elements of a software system, such as components, modules, and layers, organised in a way that promotes modularity, flexibility, and scalability.
  • Patterns and Templates. Architecture styles typically provide patterns and templates that help architects make decisions about system structure and component usage.
  • Implementation Technologies. An architecture style may specify particular implementation technologies, such as programming languages, frameworks, or libraries, chosen based on their suitability for the style.
  • Quality Attributes. They consider the quality attributes of a software system, such as performance, reliability, maintainability, and security, providing guidelines to meet these attributes.
  • Trade-offs. Architecture styles often involve trade-offs between different quality attributes or design goals, such as prioritising performance over flexibility or security over ease of use.
  • Standards and Conventions. They include standards and conventions for naming, coding style, documentation, and other aspects of software design, ensuring consistency and maintainability across the system.

Serverless Computing: A Deployment Model, Not an Architecture Style

Having defined what an architectural style encompasses, let’s explore the nature of serverless computing and how it differs fundamentally from an architecture style.

  • Deployment Model. Serverless computing is primarily a deployment model. It is often associated with “Functions as a Service” (FaaS) platforms, where developers deploy individual functions to the cloud, and the cloud provider manages the scaling and underlying infrastructure. This model focuses on how the system is deployed and managed, rather than how it is designed and structured.
  • Scope of Address. While serverless computing offers benefits such as scalability, cost-effectiveness, and increased developer productivity, it doesn’t address all aspects of architecture. For instance, it does not provide solutions for data management, security, or integration with other systems.
  • Compatibility with Various Architectures. Serverless can be used with various architectural styles, such as microservices, event-driven architecture, or even hexagonal architecture (though it is a poor choice for going serverless). This flexibility indicates that serverless is not inherently tied to any specific architecture but can be used in different contexts depending on the system’s needs.
  • Vendor Lock-in. Serverless computing often relies on proprietary services and APIs from cloud providers, which can lead to vendor lock-in. This dependency can make it challenging or costly to switch providers or bring the system in-house.

The Distinction: Architecture Style vs. Deployment Model

An architectural style provides a comprehensive template for designing your entire IT system, irrespective of its size. It encompasses a broad set of principles and guidelines for system structure, component organisation, and quality attributes. Serverless computing, on the other hand, is a runtime environment to which you can deploy parts of your application. It serves as an implementation platform for applications designed using a particular architectural style.

In summary, while serverless computing offers a powerful and flexible deployment model, it does not fulfil the role of an architectural style. Instead, it complements various architectural styles by providing a scalable, cost-effective environment for running application components. Understanding this distinction is crucial for effectively leveraging serverless computing in your IT solutions.