Back in its startup days, Circit operated with a small team of under 10 employees, juggling multiple products and facing limited resources and expertise in the emerging field of Open Banking. With the advent of PSD2 and the industry’s push towards standardization, an opportunity emerged to revolutionize how auditors accessed banking data. However, the complexity of the task seemed daunting, especially given Circit’s nascent position in the market.

Recognizing the need to adopt a structured approach, we embarked on a journey familiar to many software products: defining a domain model, establishing ubiquitous terminologies, creating a canonical model, and crafting a set of abstractions. These foundational steps provided a roadmap for future development but also highlighted the challenge of making concrete decisions with limited resources and expertise.

To overcome this hurdle, Circit made a strategic decision to leverage existing frameworks, particularly focusing on the UK market where the company had its primary operations. By aligning with the UK’s Open Banking framework, Circit was able to accelerate its development process and deliver solutions tailored to local bank integrations.

However, as the company expanded into new markets, such as Europe and beyond, it faced a new set of challenges. Many of the integration requests came from non-UK-based banks, rendering the existing domain model and terminologies obsolete. Once again, Circit found itself at a crossroads, needing to revisit its approach to accommodate diverse market requirements.

Realizing the impracticality of building bespoke integrations for each new market, we adopted a strategy rooted in the principles of split and operate. As new banking frameworks like XS2A, STET, and EBICS emerged, we transformed them into what they termed ‘Protocols’, eventually they are sub-domains. These Protocols encapsulated the core functionalities of each framework, providing a standardized interface for integration.

However, the challenge lay in the diverse implementation options offered by banks within each framework. For instance, while the STET protocol provided a standardized set of features, individual banks could implement it with variations in client authorization approaches, such as Basic, JWT, or TLS. We recognized the need for a flexible solution capable of accommodating these variations without sacrificing efficiency or scalability.

To address this challenge, we leveraged the power of virtualization and abstraction within the C# programming language. By abstracting away the specific implementation details and creating virtual representations of banking protocols, we developed a modular and extensible architecture.

// Define an abstract base class for banking protocols
public abstract class BankingProtocol
{
    // Define common methods and properties for all protocols
    public abstract void HandleAuthorization(string authorizationMethod);
}

// Define concrete implementations for specific banking protocols
public class STETProtocol : BankingProtocol
{
    public override void HandleAuthorization(string authorizationMethod)
    {
        // Implement STET-specific authorization handling logic
        Console.WriteLine($"Handling authorization for STET protocol using {authorizationMethod} method.");
    }
}

public class XS2AProtocol : BankingProtocol
{
    public override void HandleAuthorization(string authorizationMethod)
    {
        // Implement XS2A-specific authorization handling logic
        Console.WriteLine($"Handling authorization for XS2A protocol using {authorizationMethod} method.");

  • We define an abstract base class BankingProtocol with a method HandleAuthorization, representing the common behavior for all banking protocols.
  • Concrete implementations (STETProtocol and XS2AProtocol) inherit from the base class and provide protocol-specific logic for handling authorization.
  • We use a factory class ProtocolFactory to create instances of specific protocols based on the provided protocol name.
  • In the Main method, we demonstrate how to use the factory to create a protocol instance and invoke the HandleAuthorization method

.

In adopting a design approach that prioritized isolation per API provider, we effectively minimized redundancy and streamlined development efforts. Despite facing protocols that were nearly identical across various providers, the company implemented a system that ensured each provider operated independently. This approach, rooted in the principles of procedural and conceptual abstraction, enabled us to significantly reduce development overhead. Rather than duplicating code for similar protocols, we leveraged the power of dependency injection and configuration. By doing so, the company eliminated the need for repetitive coding tasks, allowing developers to focus on configuring dependencies rather than rewriting code. This proactive strategy not only mitigated the risk of code duplication but also enhanced maintainability and scalability

public abstract class ApiProvider
{
    public abstract void HandleRequest(string requestData);
}

// Define concrete implementations for API providers corresponding to different protocols
public class STETApiProvider : ApiProvider
{
    public override void HandleRequest(string requestData)
    {
        // Implement logic specific to handling requests for the STET protocol
        Console.WriteLine("Handling request using STET protocol: " + requestData);
    }
}

public class XS2AApiProvider : ApiProvider
{
    public override void HandleRequest(string requestData)
    {
        // Implement logic specific to handling requests for the XS2A protocol
        Console.WriteLine("Handling request using XS2A protocol: " + requestData);
    }
}

In this code example:

  • We define an abstract base class ApiProvider representing the common behavior for all API providers.
  • Concrete implementations (STETApiProvider and XS2AApiProvider) inherit from the base class and provide protocol-specific logic for handling requests.
  • We use a factory class ApiProviderFactory to create instances of API providers based on the provided protocol name.
  • In the Main method, we demonstrate how to use the factory to create an API provider based on the specified protocol name and simulate handling a request using the created provider.

With the foundational low-level and protocol-level abstractions in place, we can now establish domain-level abstractions that encapsulate each API provider.

using System;

// Define an abstract base class for API providers
public abstract class ApiProvider
{
    public abstract void HandleRequest(string requestData);
}

// Define concrete implementations for API providers corresponding to different protocols
public class STETApiProvider : ApiProvider
{
    public override void HandleRequest(string requestData)
    {
        // Implement logic specific to handling requests for the STET protocol
        Console.WriteLine("Handling request using STET protocol: " + requestData);
    }
}

public class XS2AApiProvider : ApiProvider
{
    public override void HandleRequest(string requestData)
    {
        // Implement logic specific to handling requests for the XS2A protocol
        Console.WriteLine("Handling request using XS2A protocol: " + requestData);
    }
}

In this updated example:

  • We introduce a BankApi class representing a domain-level abstraction for an API provider. This class encapsulates the underlying low-level protocol-specific API provider.
  • The BankApi class takes a protocol name as a parameter during instantiation and internally creates the corresponding low-level API provider using the ApiProviderFactory.
  • The HandleRequest method of the BankApi class delegates the request handling to the underlying API provider.

Utilizing these domain-level abstractions enables us to seamlessly implement additional integrations without interfering with existing protocols, simply by implementing new domain-level abstractions.

Now we introduced you to the first Pattern we use called Abstract Factory.

public static class ApiProviderFactory
{
    public static ApiProvider CreateApiProvider(string protocolName)
    {
        // Logic to determine which API provider to instantiate based on the protocol name
        switch (protocolName.ToUpper())
        {
            case "STET":
                return new STETApiProvider();
            case "XS2A":
                return new XS2AApiProvider();
            default:
                throw new ArgumentException("Unsupported protocol name.");
        }
    }
}

// Define a domain-level abstraction representing an API provider
public class BankApi
{
    private readonly ApiProvider _apiProvider;

    public BankApi(string protocolName)
    {
        // Create an API provider based on the specified protocol name
        _apiProvider = ApiProviderFactory.CreateApiProvider(protocolName);
    }

    // Method to handle a request using the underlying API provider
    public void HandleRequest(string requestData)
    {
        _apiProvider.HandleRequest(requestData);
    }
}

In this example:

  • We introduce a BankApi class representing a domain-level abstraction for an API provider. This class encapsulates the underlying low-level protocol-specific API provider.
  • The BankApi class takes a protocol name as a parameter during instantiation and internally creates the corresponding low-level API provider using the ApiProviderFactory.
  • The HandleRequest method of the BankApi class delegates the request handling to the underlying API provider.

Integration processes comprise two pivotal components: Authorization and Data Retrieval. Both of these elements are abstracted in a manner akin to what was described earlier, beginning with authorization.

Initially, our approach centered around a redirect authentication flow. However, as our operations expanded, we delved into a myriad of alternative methods, including Embedded and Decoupled approaches. Furthermore, a significant portion of banks implement multiple authorization flows, adding layers of complexity to the landscape. Consequently, each API provider and protocol can be conceptualized as a collection of abstractions. This segues seamlessly into our next topic of discussion: the Strategy pattern.

// Define an abstract base class for Authorization strategies
public abstract class AuthorizationStrategy
{
    public abstract void Authorize();
}

// Concrete implementations for Authorization strategies
public class RedirectAuthorization : AuthorizationStrategy
{
    public override void Authorize()
    {
        Console.WriteLine("Performing redirect authorization...");
    }
}

public class EmbeddedAuthorization : AuthorizationStrategy
{
    public override void Authorize()
    {
        Console.WriteLine("Performing embedded authorization...");
    }
}

// Factory class to create instances of Authorization strategies based on the approach
public static class AuthorizationStrategyFactory
{
    public static AuthorizationStrategy CreateAuthorizationStrategy(string approach)
    {
        switch (approach.ToUpper())
        {
            case "REDIRECT":
                return new RedirectAuthorization();
            case "EMBEDDED":
                return new EmbeddedAuthorization();
            // Add more cases for other authorization approaches as needed
            default:
                throw new ArgumentException("Unsupported authorization approach.");
        }
    }
}

// Domain-level abstraction representing authorization for a specific API provider
public class AuthorizationProcess
{
    private readonly AuthorizationStrategy _authorizationStrategy;

    public AuthorizationProcess(string approach)
    {
        _authorizationStrategy = AuthorizationStrategyFactory.CreateAuthorizationStrategy(approach);
    }

    public void PerformAuthorization()
    {
        _authorizationStrategy.Authorize();
    }
}

In this example:

  • We introduce a new set of abstractions for authorization processes, following the same structure as the API providers.
  • The AuthorizationStrategyFactory creates instances of concrete authorization strategies based on the provided approach.
  • The AuthorizationProcess class encapsulates the selected authorization strategy and provides a method to perform the authorization process.
  • The Main method demonstrates how to use the authorization process, similar to how we used the API provider in the previous example.

With a strategic pivot, we embarked on a quest to encapsulate the myriad authorization approaches that lay before them. From the familiar redirect flow to the intricacies of embedded and decoupled authorization, each method found its representation within Circit’s evolving architecture.

Underpinning this transformation was the establishment of a foundational abstraction: the AuthorizationStrategy. This abstract entity served as the blueprint for the myriad authorization approaches we would encounter. Concrete implementations such as RedirectAuthorization and EmbeddedAuthorization breathed life into these abstractions, each representing a unique facet of the authorization landscape.

But abstraction alone was not enough. At Circit we needed a mechanism to instantiate these strategies seamlessly, adapting to the nuanced requirements of each integration. Thus, the AuthorizationStrategyFactory was born. With this factory at their disposal, we could conjure the precise authorization strategy needed for any given scenario, whether it be the simplicity of a redirect or the sophistication of an embedded flow.

As the dust settled and Circit’s system matured, the AuthorizationProcess emerged as the linchpin of their authorization framework. This domain-level abstraction served as the conduit between strategy and execution, orchestrating the intricacies of authorization with finesse and precision.

Armed with the Strategy pattern and fortified by domain-level abstractions, We stood poised to tackle the ever-shifting landscape of banking integration. With each new challenge, they embraced the versatility and adaptability afforded by their strategic design, forging ahead into a future ripe with possibility and innovation.

When it comes to the data retrieval process, our focus lies on three primary operations: fetching all accounts, retrieving balances for each account, and obtaining transaction details. This succinctly encapsulates the complexity of our data retrieval mechanism.

To streamline this process, we employ the Template Method pattern. This strategic choice enables us to construct a cohesive pipeline using abstractions, facilitating seamless data retrieval.

// Abstract class representing the data retrieval process
public abstract class DataRetrievalProcess
{
    // Template method defining the data retrieval pipeline
    public void RetrieveData()
    {
        // Step 1: Fetch all accounts
        List<Account> accounts = FetchAllAccounts();

        // Step 2: Retrieve balances for each account
        foreach (var account in accounts)
        {
            decimal balance = RetrieveBalance(account);
            Console.WriteLine($"Balance for Account {account.AccountNumber}: {balance}");
        }

        // Step 3: Retrieve transaction details for each account
        foreach (var account in accounts)
        {
            List<Transaction> transactions = RetrieveTransactions(account);
            Console.WriteLine($"Transactions for Account {account.AccountNumber}:");
            foreach (var transaction in transactions)
            {
                Console.WriteLine(transaction);
            }
        }
    }

    // Abstract methods representing steps in the data retrieval process
    protected abstract List<Account> FetchAllAccounts();
    protected abstract decimal RetrieveBalance(Account account);
    protected abstract List<Transaction> RetrieveTransactions(Account account);
}

// Sample Account class
public class Account
{
    public string AccountNumber { get; set; }
    // Other properties
}

// Sample Transaction class
public class Transaction
{
    // Transaction properties
}

// Concrete implementation of the data retrieval process
public class ConcreteDataRetrievalProcess : DataRetrievalProcess
{
    // Mock data for demonstration purposes
    protected override List<Account> FetchAllAccounts()
    {
        return new List<Account>
        {
            new Account { AccountNumber = "123456789" },
            new Account { AccountNumber = "987654321" }
        };
    }

    protected override decimal RetrieveBalance(Account account)
    {
        // Mock balance retrieval logic
        return 1000.00m; // Dummy balance value
    }

    protected override List<Transaction> RetrieveTransactions(Account account)
    {
        // Mock transaction retrieval logic
        return new List<Transaction>
        {
            new Transaction { /* Transaction details */ },
            new Transaction { /* Transaction details */ }
        };
    }
}

In this code example:

  • We’ve established an abstract class DataRetrievalProcess to encapsulate the data retrieval workflow. It furnishes a template method RetrieveData() delineating the sequence encompassing fetching accounts, obtaining balances, and retrieving transactions.
  • Concrete subclasses offer implementations for FetchAllAccounts(), RetrieveBalance(), and RetrieveTransactions(), tailoring the behavior to distinct data sources.
  • A concrete implementation, ConcreteDataRetrievalProcess, furnishes mock data retrieval logic, showcasing the practical application of the pattern.
  • In the Main method, we instantiate ConcreteDataRetrievalProcess and execute the data retrieval process.

As we discussed previously, managing hundreds of integrations entails accommodating diverse data contracts and interpretations across various platforms. Each integration brings its own nuances, such as differing interpretations of balance types. However, amidst this complexity, we recognized the opportunity to leverage a canonical data model, providing a unified representation of essential data points.

This canonical data model serves as our foundation, offering a standardized format for handling data across integrations. It enables us to identify and prioritize valuable data based on client needs, ensuring consistency and coherence in our operations.

To bridge the gap between the canonical data model and the unique requirements of each API provider, we’ve introduced a new pattern: the Adapter pattern. At the API provider level, adapters are responsible for translating data from the canonical model into the specific format required by each integration. This delegation of responsibility ensures seamless data unification while accommodating the idiosyncrasies of individual platforms.

// Canonical data model representing essential data points
public class CanonicalDataModel
{
    public string AccountNumber { get; set; }
    public decimal Balance { get; set; }
    // Other properties relevant to the canonical model
}

// Interface for API provider adapters
public interface IApiProviderAdapter
{
    void ConvertAndSendData(CanonicalDataModel data);
}

// Concrete adapter for API provider A
public class ApiProviderAAdapter : IApiProviderAdapter
{
    public void ConvertAndSendData(CanonicalDataModel data)
    {
        // Convert canonical data to format specific to API provider A
        Console.WriteLine("Converting data to format specific to API provider A:");
        Console.WriteLine($"Account: {data.AccountNumber}, Balance: {data.Balance}");
        // Send data to API provider A
        Console.WriteLine("Sending data to API provider A...");
    }
}

// Concrete adapter for API provider B
public class ApiProviderBAdapter : IApiProviderAdapter
{
    public void ConvertAndSendData(CanonicalDataModel data)
    {
        // Convert canonical data to format specific to API provider B
        Console.WriteLine("Converting data to format specific to API provider B:");
        Console.WriteLine($"Account: {data.AccountNumber}, Balance: {data.Balance}");
        // Send data to API provider B
        Console.WriteLine("Sending data to API provider B...");
    }
}

// Client class responsible for orchestrating data unification and sending to API providers
public class DataUnificationClient
{
    private readonly List<IApiProviderAdapter> _adapters;

    public DataUnificationClient()
    {
        // Initialize adapters for different API providers
        _adapters = new List<IApiProviderAdapter>
        {
            new ApiProviderAAdapter(),
            new ApiProviderBAdapter()
            // Add more adapters for other API providers as needed
        };
    }

    // Method to unify data and send to all API providers
    public void UnifyAndSendData(CanonicalDataModel data)
    {
        foreach (var adapter in _adapters)
        {
            adapter.ConvertAndSendData(data);
        }
    }
}

In this example:

  • We define a canonical data model (CanonicalDataModel) representing essential data points shared across different API providers.
  • We create an interface IApiProviderAdapter representing the adapter pattern, with a method ConvertAndSendData to convert canonical data and send it to the respective API provider.
  • Concrete adapter classes (ApiProviderAAdapter and ApiProviderBAdapter) implement the IApiProviderAdapter interface to convert canonical data into formats specific to API providers A and B, respectively.
  • The DataUnificationClient class orchestrates the data unification process by iterating through a list of adapters and sending the converted data to all API providers.
  • In the Main method, we create an instance of the canonical data model, instantiate the DataUnificationClient, and unify the data, demonstrating the adapter pattern in action for data unification across multiple API providers.

In conclusion, Circit’s journey underscores the transformative power of strategic design pattern adoption in addressing the complexities of API integration. Beginning with limited resources and expertise, Circit capitalized on regulatory shifts like PSD2 to redefine auditor access to data. By embracing patterns such as the Strategy pattern, the company navigated market expansions and evolving integration demands with agility.

The adoption of a canonical data model facilitated seamless data representation across integrations, while adapters bridged the gap between the canonical model and API-specific requirements. Furthermore, Circit’s integration arsenal was bolstered by additional patterns like retry mechanisms and parallel processing, ensuring reliability and performance optimization.

In essence, Circit’s success story exemplifies how strategic design pattern adoption empowers organizations to adapt to regulatory changes, meet evolving market demands, and unlock new levels of efficiency and innovation in API integration.

As we conclude our discussion on Circit’s innovative solutions for API integration and data management, we invite you to explore some intriguing questions:

  1. Consent API in details: How does Circit’s Consent API ensure user privacy while enabling seamless access to critical data for auditing purposes?
  2. Article 10 in details: What insights does Article 10 offer within Circit’s documentation, and how does it contribute to our understanding of regulatory compliance and data handling protocols?
  3. MATLS.
  4. Authentication Flows Demystified: Would you like to dive deeper into Circit’s authentication flows, such as Redirect, Decoupled, and Embedded authentication? Learn how each flow works and their implications for security and user experience.
  5. Dynamic Client Registration: Curious about Dynamic Client Registration and its role in OAuth 2.0? Explore its significance in Circit’s ecosystem and how it facilitates seamless integration with various API providers.

We’re excited to delve into these topics in future publications, providing you with a comprehensive understanding of Circit’s technology stack and regulatory strategies. Stay tuned for more insights and discoveries!

Download pdf
Request a demo

See what Circit can do for your firm