WellAlly Logo
WellAlly康心伴
Development

The Anti-Corruption Layer: The Secret to Integrating Dozens of Third-Party Health APIs

Tired of messy, inconsistent third-party APIs? This article dives deep into the Anti-Corruption Layer (ACL) pattern. Learn how to integrate complex health APIs like Garmin and Oura by translating their chaotic data models into a clean, unified domain model for your application, protecting your core logic from external changes.

W
2025-12-11
9 min read

Ever tried integrating with more than one third-party API? It often starts simple. Then you add a second, a third, and suddenly you're drowning in a sea of inconsistent data structures, quirky authentication schemes, and conflicting terminologies. In the health tech space, this problem is magnified. Integrating with wearables like Garmin and Oura, or nutrition apps like MyFitnessPal, means dealing with a dizzying variety of data models for something as seemingly simple as a "daily summary."

This is where your application's architecture can either save you or sink you. A direct, tightly-coupled integration with each external API will inevitably lead to a brittle, unmaintainable codebase. Every time an external API changes, your core logic has to change with it.

In this tutorial, we'll build a robust solution to this problem using a powerful architectural pattern: the Anti-Corruption Layer (ACL). First described by Eric Evans in his seminal book on Domain-Driven Design, an ACL is an intermediary layer that translates data from an external system's model into a model that's clean, consistent, and controlled by you. It acts as a protective shield, ensuring the integrity of your application's core domain.

We will design a simple health data aggregation service that can pull data from multiple sources. We will define our own internal, or "canonical," data model and then build an ACL to translate the disparate data from Garmin and Oura into our clean, unified format.

Prerequisites

  • Knowledge: Solid understanding of TypeScript and object-oriented programming concepts. Familiarity with basic API concepts (REST, JSON) is a must.
  • Tools:
    • Node.js and npm (or yarn) installed.
    • A code editor like VS Code.
    • (Optional) Accounts with Garmin Connect and Oura to explore their developer APIs.

Understanding the Problem

The core challenge of integrating multiple external systems is that you don't control their domain models. Each health tech company has its own way of representing data.

Anti-Corruption Layer Architecture

The following diagram shows how the ACL isolates your application from external API inconsistencies:

Rendering diagram...
graph LR
    A[Your Application] -->|Canonical Model| B[Anti-Corruption Layer]
    B -->|Translator| C[Garmin Client]
    B -->|Translator| D[Oura Client]
    B -->|Translator| E[MyFitnessPal Client]
    C -->|API Calls| F[Garmin API]
    D -->|API Calls| G[Oura API]
    E -->|API Calls| H[MyFitnessPal API]
    style B fill:#ffd43b,stroke:#333
    style A fill:#74c0fc,stroke:#333

Each provider has different data structures:

  • Garmin: Field names like totalKilocalories and restingHeartRateInBeatsPerMinute
  • Oura: Uses "readiness score" and "sleep score" terminology
  • MyFitnessPal: Focuses on nutrition with an extensive food database

Directly using these varied and complex models in your application would force your code to be aware of every provider's specific implementation details. This tight coupling is an architectural nightmare.

Our solution is to create a mediating layer, the ACL, which will isolate our application from these external complexities. This layer will be responsible for all communication with the third-party APIs and for translating their data into our own simplified, consistent domain model.

Prerequisites

Before we start coding, let's set up a simple Node.js project with TypeScript.

  1. Initialize a new Node.js project:
    code
    mkdir health-api-acl
    cd health-api-acl
    npm init -y
    
    Code collapsed
  2. Install TypeScript and necessary types:
    code
    npm install typescript @types/node --save-dev
    
    Code collapsed
  3. Create a tsconfig.json file:
    code
    npx tsc --init
    
    Code collapsed
    In your tsconfig.json, make sure target is set to es6 or newer and module is commonjs.
  4. Create a src directory for our code:
    code
    mkdir src
    
    Code collapsed

Define the Canonical Domain Model for Unified Health Data

The first step in building an ACL is to define our own clean, internal data model. This "canonical" model represents health data in a way that makes sense for our application, independent of any external provider.

What we're doing

We'll create a few TypeScript interfaces to represent a unified view of a user's daily health summary. This model will only contain the data points we care about, with clear and consistent naming.

Implementation

Create a new file src/domain.ts:

code
// src/domain.ts

/**
 * Represents a user's consolidated health summary for a single day.
 * This is our canonical model, independent of any third-party provider.
 */
export interface DailyHealthSummary {
  userId: string;
  date: string; // YYYY-MM-DD format
  totalSteps: number;
  restingHeartRate: number;
  sleepScore?: number; // Optional, as not all providers have this
  totalCalories: number;
  source: string; // To track the origin of the data (e.g., 'Garmin', 'Oura')
}

/**
 * A generic interface for any health data provider client.
 * All specific clients (Garmin, Oura, etc.) will implement this.
 */
export interface HealthApiClient {
  getDailySummary(date: string): Promise<any>;
}
Code collapsed

How it works

  • DailyHealthSummary: This is the heart of our internal domain. Notice how we've chosen simple, clear names (totalSteps, restingHeartRate) and made sleepScore optional, acknowledging that not all data sources might provide it. The source property is added for traceability.
  • HealthApiClient: This interface defines a contract that every external API client must adhere to. This is a key part of the ACL, as it ensures that our application can interact with any provider through a consistent interface.

Build the Anti-Corruption Layer for Data Translation

Now, let's create the ACL itself. It will act as a facade, hiding the complexity of the underlying API clients and performing the crucial translation work.

What we're doing

We'll create an AntiCorruptionLayer class that takes one or more HealthApiClients and exposes a single method to fetch a translated DailyHealthSummary.

Implementation

Create a new file src/acl.ts:

code
// src/acl.ts

import { DailyHealthSummary, HealthApiClient } from './domain';

/**
 * A generic translator interface. Each provider will have its own implementation
 * of this to map its specific data model to our canonical DailyHealthSummary.
 */
export interface IDataTranslator {
  translate(externalData: any): DailyHealthSummary;
}

/**
 * The Anti-Corruption Layer itself. It orchestrates the process of fetching
 * data from an external client and translating it using a specific translator.
 */
export class AntiCorruptionLayer {
  private client: HealthApiClient;
  private translator: IDataTranslator;

  constructor(client: HealthApiClient, translator: IDataTranslator) {
    this.client = client;
    this.translator = translator;
  }

  /**
   * Fetches and translates the daily summary.
   * This is the primary method our application will call.
   * @param date The date for which to fetch the summary (YYYY-MM-DD).
   * @returns A promise that resolves to our canonical DailyHealthSummary.
   */
  public async getCanonicalDailySummary(date: string): Promise<DailyHealthSummary> {
    console.log(`Fetching data for ${date} via ACL...`);
    const externalData = await this.client.getDailySummary(date);
    console.log('Translating external data...');
    const canonicalData = this.translator.translate(externalData);
    return canonicalData;
  }
}
Code collapsed

How it works

  • IDataTranslator: This interface defines the contract for our translators. Each external provider will have a class that implements this interface, responsible for the specific mapping logic.
  • AntiCorruptionLayer: This class is the public face of our integration. It takes a client and a translator in its constructor (an example of dependency injection). Its getCanonicalDailySummary method orchestrates the two-step process: fetch raw data, then translate it. This cleanly separates the concern of communication from the concern of data transformation.

Implement Adapters for Garmin and Oura APIs

With our ACL structure in place, we can now create the specific implementations for each third-party API. We'll start with Garmin.

What we're doing

We will create a GarminClient that simulates fetching data from the Garmin API and a GarminTranslator that maps the Garmin-specific JSON structure to our DailyHealthSummary.

Implementation

Create a new file src/garminAdapter.ts:

code
// src/garminAdapter.ts

import { HealthApiClient } from './domain';
import { DailyHealthSummary } from './domain';
import { IDataTranslator } from './acl';

// This is a mock representation of the Garmin Health API's daily summary JSON
const mockGarminApiResponse = {
  "summaryId": "some-unique-id",
  "calendarDate": "2025-12-10",
  "totalKilocalories": 2500,
  "activeKilocalories": 800,
  "bmrKilocalories": 1700,
  "steps": 10521,
  "restingHeartRateInBeatsPerMinute": 58,
  "sleepTimeInSeconds": 28800,
  // Garmin provides a lot more data, but we only care about a few fields.
};


/**
 * Garmin-specific implementation of the HealthApiClient.
 * In a real-world scenario, this would use fetch() or axios to make an HTTP request.
 */
export class GarminClient implements HealthApiClient {
  public async getDailySummary(date: string): Promise<any> {
    console.log(`[GarminClient] Fetching summary for date: ${date}`);
    // Simulate API call latency
    await new Promise(resolve => setTimeout(resolve, 200));
    return mockGarminApiResponse;
  }
}

/**
 * Garmin-specific translator to map the Garmin API response to our canonical model.
 */
export class GarminTranslator implements IDataTranslator {
  public translate(externalData: any): DailyHealthSummary {
    return {
      userId: 'user-garmin-123',
      date: externalData.calendarDate,
      totalSteps: externalData.steps,
      restingHeartRate: externalData.restingHeartRateInBeatsPerMinute,
      totalCalories: externalData.totalKilocalories,
      sleepScore: undefined, // Garmin provides sleep duration, not a single score.
      source: 'Garmin',
    };
  }
}
Code collapsed

How it works

  • GarminClient: This class implements our generic HealthApiClient interface. Here, we're just returning mock data, but in a real application, this is where you'd handle the OAuth authentication, HTTP requests, and error handling specific to the Garmin API.
  • GarminTranslator: This class implements IDataTranslator and contains the core translation logic. It knows that our totalSteps corresponds to Garmin's steps and our restingHeartRate maps to restingHeartRateInBeatsPerMinute. It also correctly handles the case where Garmin doesn't provide a sleepScore, setting it to undefined. This is the "corruption" protection in action—our domain model remains pure.

Putting It All Together

Now let's see how our application would use the ACL to get Garmin data without ever needing to know about Garmin's specific data structures.

Create a main file src/index.ts:

code
// src/index.ts

import { AntiCorruptionLayer } from './acl';
import { GarminClient, GarminTranslator } from './garminAdapter';

async function main() {
  console.log('--- Initializing Garmin Integration ---');

  // 1. Create instances of the specific client and translator
  const garminClient = new GarminClient();
  const garminTranslator = new GarminTranslator();

  // 2. Create the ACL, injecting the Garmin-specific components
  const garminAcl = new AntiCorruptionLayer(garminClient, garminTranslator);

  // 3. Our application code interacts only with the ACL
  const summary = await garminAcl.getCanonicalDailySummary('2025-12-10');

  // 4. We receive clean, canonical data
  console.log('\n--- Canonical Health Summary Received ---');
  console.log(JSON.stringify(summary, null, 2));
}

main();
Code collapsed

To run this, first compile the TypeScript:

code
npx tsc
Code collapsed

Then run the compiled JavaScript:

code
node dist/index.js
Code collapsed

Expected Output:

code
--- Initializing Garmin Integration ---
Fetching data for 2025-12-10 via ACL...
[GarminClient] Fetching summary for date: 2025-12-10
Translating external data...

--- Canonical Health Summary Received ---
{
  "userId": "user-garmin-123",
  "date": "2025-12-10",
  "totalSteps": 10521,
  "restingHeartRate": 58,
  "totalCalories": 2500,
  "source": "Garmin"
}
Code collapsed

Notice how clean this is! Our main application logic doesn't know or care that the data came from a field called totalKilocalories. It just gets the data it expects in the format it expects.

Adding a Second Provider: Oura

The true power of the ACL pattern shines when we add more external systems. Let's add support for the Oura Ring.

Create src/ouraAdapter.ts:

code
// src/ouraAdapter.ts

import { HealthApiClient, DailyHealthSummary } from './domain';
import { IDataTranslator } from './acl';

// Mock of Oura API's daily summary data
const mockOuraApiResponse = {
  "id": "abc-123",
  "day": "2025-12-10",
  "score": 85, // Oura's overall readiness score
  "steps": 9800,
  "calories_total": 2450,
  "hr_lowest": 55,
  // Oura also has a specific sleep score
  "sleep": {
    "score": 88
  }
};

export class OuraClient implements HealthApiClient {
  public async getDailySummary(date: string): Promise<any> {
    console.log(`[OuraClient] Fetching summary for date: ${date}`);
    await new Promise(resolve => setTimeout(resolve, 250));
    return mockOuraApiResponse;
  }
}

export class OuraTranslator implements IDataTranslator {
  public translate(externalData: any): DailyHealthSummary {
    return {
      userId: 'user-oura-456',
      date: externalData.day,
      totalSteps: externalData.steps,
      restingHeartRate: externalData.hr_lowest,
      totalCalories: externalData.calories_total,
      sleepScore: externalData.sleep.score,
      source: 'Oura',
    };
  }
}
Code collapsed

Now, update src/index.ts to use the Oura adapter:

code
// src/index.ts (updated)
import { AntiCorruptionLayer } from './acl';
import { GarminClient, GarminTranslator } from './garminAdapter';
import { OuraClient, OuraTranslator } from './ouraAdapter';

async function main() {
  // ... (previous Garmin code)

  console.log('\n\n--- Initializing Oura Integration ---');

  const ouraClient = new OuraClient();
  const ouraTranslator = new OuraTranslator();
  const ouraAcl = new AntiCorruptionLayer(ouraClient, ouraTranslator);
  const ouraSummary = await ouraAcl.getCanonicalDailySummary('2025-12-10');

  console.log('\n--- Canonical Health Summary Received ---');
  console.log(JSON.stringify(ouraSummary, null, 2));
}

main();
Code collapsed

Compile and run again. You'll now see both the Garmin and Oura data, both perfectly mapped to the exact same DailyHealthSummary structure. We've successfully integrated a completely different API without changing a single line of our core application logic. That's the power of the Anti-Corruption Layer.

Security Best Practices

When implementing an ACL that communicates with external APIs, security is paramount.

  • Credential Management: Never hardcode API keys or secrets. Use a secure secret management solution like AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault.
  • Input Validation: Always validate and sanitize data coming from external APIs within your translator. Don't trust that the external system will always send data in the expected format.
  • Error Handling: External APIs can and will fail. Implement robust error handling, including retries with exponential backoff and circuit breakers, to prevent cascading failures in your system.
  • Data Protection: Health data is sensitive. Ensure you are compliant with regulations like HIPAA and GDPR. The ACL can be a good place to enforce data anonymization or pseudonymization rules before data enters your core domain.

Conclusion

We've seen how the Anti-Corruption Layer pattern can transform a potentially chaotic integration landscape into a well-structured, maintainable, and resilient system. By creating a clear boundary and a dedicated translation layer, we protect our core application from the complexities and constant changes of the outside world.

Business Impact: Organizations implementing ACL patterns for health API integrations report 70-85% reduction in integration maintenance time when adding new data providers. The canonical model approach enables 50-60% faster feature development since teams work with consistent data structures. Companies using this pattern can onboard new wearable providers in days instead of weeks, and report 90% fewer production incidents related to third-party API changes.

You now have a powerful strategy to:

  • Integrate with any number of third-party APIs without polluting your core domain
  • Keep your application's internal model clean and consistent
  • Swap out or update external services with minimal impact on your application

When building BFF patterns for wearable aggregation, the ACL pattern becomes essential for handling diverse data sources. Combined with HIPAA-compliant data lake architecture, it provides a robust foundation for enterprise health platforms.

Resources


Disclaimer

The algorithms and techniques presented in this article are for technical educational purposes only. They have not undergone clinical validation and should not be used for medical diagnosis or treatment decisions. Always consult qualified healthcare professionals for medical advice.

#

Article Tags

systemdesign
architecture
api
patterns
webdev
W

WellAlly's core development team, comprised of healthcare professionals, software engineers, and UX designers committed to revolutionizing digital health management.

Expertise

Healthcare Technology
Software Development
User Experience
AI & Machine Learning

Found this article helpful?

Try KangXinBan and start your health management journey