Deploy a Revenue Cycle Benchmarking API

Build and deploy a serverless REST API for healthcare KPI benchmarking on Azure.

Introduction

30 Second Summary

Every hospital wants to know how it stacks up against similar organizations. Revenue cycle teams track dozens of key performance indicators to answer that question, from how quickly claims get paid to how many get denied on first submission.

In this project, you will build and deploy a serverless API on Azure that serves revenue cycle KPI benchmarking data. You will also produce two professional artifacts that Technical Product Managers and Solution Architects create daily: an OpenAPI specification and an Architecture Decision Record.

What You'll Build

A revenue cycle KPI benchmarking API deployed as a serverless Azure Function, paired with professional documentation artifacts that prove you can design and defend infrastructure decisions.

By the end of this project, you'll have:

  • A live Azure Functions API endpoint accessible via HTTP, serving revenue cycle KPI data organized by pillar.
  • A professional OpenAPI 3.0 specification documenting your API contract for MAP Key data access.
  • An Architecture Decision Record (ADR) that articulates infrastructure trade-offs in the context of a healthcare benchmarking platform.
  • Secret Mission: Connect your project to Azure DevOps with a CI/CD pipeline that auto-deploys on push.

Are there any prerequisites?

No prior experience with Azure or API development is required. This guide walks you through setting up every tool from scratch, including Python, Azure Functions Core Tools, and your Azure account.

Design Your API with an OpenAPI Specification

Your development environment is set up and ready to go. Now it's time to design the API you'll deploy to Azure.

Before writing any code, you need a clear contract that defines exactly what your API does, what data it accepts, and what it returns. This contract is called an OpenAPI specification, and it's the artifact that Technical Product Managers own and hand to engineering teams.

In this step, get ready to:
  • Define three revenue cycle benchmarking endpoints in an OpenAPI specification.
  • Create data schemas for MAP Key definitions, benchmark submissions, and results.
  • Validate your API design decisions against real healthcare data patterns.
Understand what you are designing

You are creating a revenue cycle benchmarking API with three endpoints. Each endpoint serves a different need for organizations tracking their financial performance.

  • GET /mapkeys returns all standardized KPI definitions, with an optional filter by revenue cycle pillar.
  • GET /mapkeys/{id} returns a single KPI along with any benchmark data that has been submitted for it.
  • POST /benchmarks accepts a new benchmark data point from a healthcare organization and returns a percentile ranking.

What is an OpenAPI specification?

An OpenAPI specification is a standardized YAML or JSON document that describes a REST API contract. It defines the available endpoints, their request parameters, response shapes, and error codes.

Frontend teams, partner integrations, and automated tools all read this document to understand how to interact with your API. The spec stays stable even when backend implementation details change.

Create the OpenAPI specification file

Your API contract will live in a YAML file inside a dedicated project folder. YAML is the industry standard format for hand-authored API specs because it is more readable than JSON.

  • In VS Code, open the integrated terminal by pressing Ctrl+` (macOS: Cmd+`).
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir RevCycleBenchmarkAPI
cd RevCycleBenchmarkAPI
  • Confirm the folder exists by running:
ls

You should see an empty directory listed (no files yet).

  • Open this folder in VS Code by running:
code .

VS Code didn't open?

If the code command is not recognized, open VS Code manually. Click File in the menu bar, then Open Folder, and select the RevCycleBenchmarkAPI folder on your Desktop.

Help me open my project folder in VS Code

  • In the VS Code Explorer sidebar (left panel), hover over the REVCYCLEBENCHMARKAPI heading and click the New File icon (the page icon with a plus sign).
  • Type openapi.yaml as the file name and press Enter.
  • Paste the following complete OpenAPI specification into openapi.yaml:
openapi: 3.0.3
info:
  title: Revenue Cycle Benchmarking API
  description: A serverless API for healthcare revenue cycle KPI benchmarking. Provides MAP Key definitions organized by pillar and accepts benchmark data submissions from healthcare organizations.
  version: 1.0.0
servers:
  - url: http://localhost:7071/api
    description: Local development
  - url: https://{functionAppName}.azurewebsites.net/api
    description: Azure production
paths:
  /mapkeys:
    get:
      summary: List all MAP Keys
      operationId: listMapKeys
      description: Returns all standardized MAP Key KPI definitions. Optionally filter by revenue cycle pillar.
      parameters:
        - name: pillar
          in: query
          required: false
          schema:
            type: string
            enum:
              - Patient Access
              - Pre-Billing
              - Claims Adjudication
              - Account Resolution
              - Financial Management
          description: Filter MAP Keys by revenue cycle pillar
      responses:
        "200":
          description: A list of MAP Key definitions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/MapKey"
  /mapkeys/{id}:
    get:
      summary: Get a single MAP Key with benchmark submissions
      operationId: getMapKey
      description: Returns a MAP Key definition including any benchmark data submissions.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: The MAP Key ID
      responses:
        "200":
          description: MAP Key details with benchmark submissions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MapKeyDetail"
        "404":
          description: MAP Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /benchmarks:
    post:
      summary: Submit a benchmark data point
      operationId: submitBenchmark
      description: Submit a monthly benchmark value for a specific MAP Key. Returns the submitted value with a calculated percentile ranking against peer organizations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BenchmarkSubmission"
      responses:
        "201":
          description: Benchmark submitted with percentile ranking
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BenchmarkResult"
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Referenced MAP Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    MapKey:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: Pre-Registration Rate
        pillar:
          type: string
          example: Patient Access
        description:
          type: string
          example: Percentage of patients pre-registered before date of service
        unit:
          type: string
          enum: [percentage, days, currency]
          example: percentage
        direction:
          type: string
          enum: [higher_is_better, lower_is_better]
          example: higher_is_better
        benchmark_median:
          type: number
          format: float
          example: 85.0
        benchmark_top_quartile:
          type: number
          format: float
          example: 95.0
    MapKeyDetail:
      allOf:
        - $ref: "#/components/schemas/MapKey"
        - type: object
          properties:
            submissions:
              type: array
              items:
                $ref: "#/components/schemas/BenchmarkResult"
    BenchmarkSubmission:
      type: object
      required:
        - mapkey_id
        - organization
        - value
        - period
      properties:
        mapkey_id:
          type: integer
          description: The ID of the MAP Key being reported
          example: 1
        organization:
          type: string
          description: Name of the submitting healthcare organization
          example: General Hospital
        value:
          type: number
          format: float
          description: The reported metric value
          example: 92.5
        period:
          type: string
          description: Reporting period in YYYY-MM format
          example: "2024-03"
    BenchmarkResult:
      type: object
      properties:
        id:
          type: integer
          example: 1
        mapkey_id:
          type: integer
          example: 1
        mapkey_name:
          type: string
          example: Pre-Registration Rate
        organization:
          type: string
          example: General Hospital
        value:
          type: number
          format: float
          example: 92.5
        period:
          type: string
          example: "2024-03"
        percentile_rank:
          type: number
          format: float
          description: Percentile ranking against peer organizations (0-100)
          example: 68.8
        performance:
          type: string
          enum: [Above Median, Below Median]
          example: Above Median
    Error:
      type: object
      properties:
        error:
          type: string

What does this specification define?

This file is structured into three main sections:

  • info and servers describe the API title, purpose, and where it runs (locally and on Azure).
  • paths defines the three endpoints with their HTTP methods, parameters, and response codes.
  • components/schemas defines reusable data shapes (MapKey, BenchmarkSubmission, BenchmarkResult, Error) so each endpoint can reference them.
  • Save openapi.yaml by pressing Ctrl+S (macOS: Cmd+S).
  • In the Explorer sidebar, you should see openapi.yaml listed under your RevCycleBenchmarkAPI folder.

File not showing in the Explorer?

Make sure you opened the RevCycleBenchmarkAPI folder itself in VS Code, not a parent folder. The folder name should appear as the top-level heading in the Explorer sidebar.

If you see Desktop as the top-level heading instead, click File then Open Folder and select RevCycleBenchmarkAPI from your Desktop.

Help me find my openapi.yaml file in VS Code

✔️ Awesome, I've got everything!

Great. Double check you saved openapi.yaml and it shows in your Explorer sidebar.

ⓧ I'd like to double check the full code

Here is the complete openapi.yaml file for comparison:

openapi: 3.0.3
info:
  title: Revenue Cycle Benchmarking API
  description: A serverless API for healthcare revenue cycle KPI benchmarking. Provides MAP Key definitions organized by pillar and accepts benchmark data submissions from healthcare organizations.
  version: 1.0.0
servers:
  - url: http://localhost:7071/api
    description: Local development
  - url: https://{functionAppName}.azurewebsites.net/api
    description: Azure production
paths:
  /mapkeys:
    get:
      summary: List all MAP Keys
      operationId: listMapKeys
      description: Returns all standardized MAP Key KPI definitions. Optionally filter by revenue cycle pillar.
      parameters:
        - name: pillar
          in: query
          required: false
          schema:
            type: string
            enum:
              - Patient Access
              - Pre-Billing
              - Claims Adjudication
              - Account Resolution
              - Financial Management
          description: Filter MAP Keys by revenue cycle pillar
      responses:
        "200":
          description: A list of MAP Key definitions
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/MapKey"
  /mapkeys/{id}:
    get:
      summary: Get a single MAP Key with benchmark submissions
      operationId: getMapKey
      description: Returns a MAP Key definition including any benchmark data submissions.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: The MAP Key ID
      responses:
        "200":
          description: MAP Key details with benchmark submissions
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MapKeyDetail"
        "404":
          description: MAP Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /benchmarks:
    post:
      summary: Submit a benchmark data point
      operationId: submitBenchmark
      description: Submit a monthly benchmark value for a specific MAP Key. Returns the submitted value with a calculated percentile ranking against peer organizations.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BenchmarkSubmission"
      responses:
        "201":
          description: Benchmark submitted with percentile ranking
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BenchmarkResult"
        "400":
          description: Invalid request body
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Referenced MAP Key not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  schemas:
    MapKey:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: Pre-Registration Rate
        pillar:
          type: string
          example: Patient Access
        description:
          type: string
          example: Percentage of patients pre-registered before date of service
        unit:
          type: string
          enum: [percentage, days, currency]
          example: percentage
        direction:
          type: string
          enum: [higher_is_better, lower_is_better]
          example: higher_is_better
        benchmark_median:
          type: number
          format: float
          example: 85.0
        benchmark_top_quartile:
          type: number
          format: float
          example: 95.0
    MapKeyDetail:
      allOf:
        - $ref: "#/components/schemas/MapKey"
        - type: object
          properties:
            submissions:
              type: array
              items:
                $ref: "#/components/schemas/BenchmarkResult"
    BenchmarkSubmission:
      type: object
      required:
        - mapkey_id
        - organization
        - value
        - period
      properties:
        mapkey_id:
          type: integer
          description: The ID of the MAP Key being reported
          example: 1
        organization:
          type: string
          description: Name of the submitting healthcare organization
          example: General Hospital
        value:
          type: number
          format: float
          description: The reported metric value
          example: 92.5
        period:
          type: string
          description: Reporting period in YYYY-MM format
          example: "2024-03"
    BenchmarkResult:
      type: object
      properties:
        id:
          type: integer
          example: 1
        mapkey_id:
          type: integer
          example: 1
        mapkey_name:
          type: string
          example: Pre-Registration Rate
        organization:
          type: string
          example: General Hospital
        value:
          type: number
          format: float
          example: 92.5
        period:
          type: string
          example: "2024-03"
        percentile_rank:
          type: number
          format: float
          description: Percentile ranking against peer organizations (0-100)
          example: 68.8
        performance:
          type: string
          enum: [Above Median, Below Median]
          example: Above Median
    Error:
      type: object
      properties:
        error:
          type: string
Validate your API design decisions

Now that your spec exists, take a moment to review the design choices baked into it. These choices reflect how real benchmarking platforms structure KPI metadata.

  • Scroll through the paths section in your openapi.yaml. Notice how each endpoint has defined response codes (200, 201, 400, 404) with specific error schemas.
  • Look at the MapKey schema in the components section. Each KPI has a direction field (higher_is_better or lower_is_better) because not all metrics improve in the same direction.
  • Notice the pillar query parameter on GET /mapkeys uses an enum to restrict values to the five revenue cycle pillars: Patient Access, Pre-Billing, Claims Adjudication, Account Resolution, and Financial Management.

Why anonymous authentication for the MVP?

Your spec does not define any authentication. This is intentional for a prototype. In production, benchmark data is sensitive organizational financial data that would require OAuth 2.0 scoping per organization.

The API contract (your OpenAPI spec) stays the same regardless of which auth layer you add later. That separation is the power of designing API-first.

Your API contract is locked in. Next up, you'll bring this specification to life by building the actual Azure Functions code that implements these endpoints.

Build the Azure Functions API Locally

Your OpenAPI specification defines what your API should do. Now it is time to bring that contract to life with actual working code.

In this step, you will scaffold an Azure Functions project, implement the three endpoints from your spec, and test them locally. By the end, you will have a running API that serves MAP Key data from memory.

In this step, get ready to:
  • Create an Azure Functions project using the VS Code wizard.
  • Implement the three API endpoints for MAP Key data.
  • Run and test your API locally to verify all endpoints respond correctly.
Create the Azure Functions project

The Azure Functions extension in VS Code provides a guided wizard that scaffolds all the configuration files your project needs. This saves you from manually writing boilerplate.

  • In VS Code, press F1 to open the command palette.
  • Type Azure Functions: Create New Project... and select it.
  • Select your RevCycleBenchmarkAPI folder as the project location.
  • Select Python as the language.
  • Select your Python 3.11 interpreter when prompted to create a virtual environment.
  • Select HTTP trigger as the template for your first function.
  • Enter HttpExample as the function name.
  • Select ANONYMOUS for the authorization level.
  • Select Open in current window when asked how to open the project.

What did the wizard generate?

The wizard created several files in your RevCycleBenchmarkAPI folder. function_app.py is where your function code lives. host.json configures the Azure Functions runtime. local.settings.json stores environment variables for local development.

It also created a Python virtual environment and a requirements.txt for managing dependencies. You will replace the generated function code with your benchmarking API in the next substep.

  • Confirm the files exist by checking the Explorer sidebar in VS Code. You should see function_app.py, host.json, local.settings.json, and requirements.txt listed alongside your existing openapi.yaml.

Don't see the generated files?

Make sure you selected the RevCycleBenchmarkAPI folder (not a parent folder) when the wizard asked for the project location. If files landed in the wrong place, delete them and run the wizard again by pressing F1 and selecting Azure Functions: Create New Project... once more.

help me troubleshoot why the Azure Functions wizard didn't generate files in my project folder

Replace the generated code with your benchmarking API

The wizard generated a simple "Hello World" function. You will now replace it with the full benchmarking API that implements the three endpoints from your OpenAPI spec.

  • Open function_app.py in the editor.
  • Select all the existing code (Ctrl+A) and delete it.
  • Paste the imports, app instance, and MAP Key data model by copying this code:
import azure.functions as func
import logging
import json

app = func.FunctionApp()

# In-memory MAP Key definitions organized by revenue cycle pillar
mapkeys = [
    {
        "id": 1,
        "name": "Pre-Registration Rate",
        "pillar": "Patient Access",
        "description": "Percentage of patients pre-registered before date of service",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 85.0,
        "benchmark_top_quartile": 95.0
    },
    {
        "id": 2,
        "name": "Point-of-Service Collections",
        "pillar": "Patient Access",
        "description": "Percentage of expected patient responsibility collected at time of service",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 32.0,
        "benchmark_top_quartile": 52.0
    },
    {
        "id": 3,
        "name": "Billing Lag Days",
        "pillar": "Pre-Billing",
        "description": "Average number of days from date of discharge to claim submission",
        "unit": "days",
        "direction": "lower_is_better",
        "benchmark_median": 5.5,
        "benchmark_top_quartile": 3.2
    },
    {
        "id": 4,
        "name": "Clean Claim Rate",
        "pillar": "Claims Adjudication",
        "description": "Percentage of claims accepted on first submission without edits or rejections",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 90.0,
        "benchmark_top_quartile": 96.0
    },
    {
        "id": 5,
        "name": "Net Days in A/R",
        "pillar": "Financial Management",
        "description": "Average number of days that net accounts receivable is outstanding",
        "unit": "days",
        "direction": "lower_is_better",
        "benchmark_median": 42.0,
        "benchmark_top_quartile": 33.0
    },
    {
        "id": 6,
        "name": "Denial Rate",
        "pillar": "Account Resolution",
        "description": "Percentage of claims denied by payers on initial submission",
        "unit": "percentage",
        "direction": "lower_is_better",
        "benchmark_median": 8.5,
        "benchmark_top_quartile": 4.0
    },
]

# In-memory benchmark submissions
benchmarks = []

What does this code do?

This sets up the foundation for your API. func.FunctionApp() creates the main app instance that Azure Functions uses to discover your endpoints.

The mapkeys list contains 6 KPI definitions spanning all 5 revenue cycle pillars. Each MAP Key includes its benchmark median and top quartile values, which the percentile calculation will use later. The benchmarks list stores submitted data points in memory.

  • Directly below the benchmarks = [] line, paste the first endpoint function that lists MAP Keys:


@app.route(route="mapkeys", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def list_mapkeys(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("GET /api/mapkeys - Listing MAP Keys")

    pillar = req.params.get("pillar")
    if pillar:
        filtered = [k for k in mapkeys if k["pillar"].lower() == pillar.lower()]
        return func.HttpResponse(
            json.dumps(filtered),
            mimetype="application/json",
            status_code=200
        )

    return func.HttpResponse(
        json.dumps(mapkeys),
        mimetype="application/json",
        status_code=200
    )

What does this endpoint do?

The @app.route decorator registers this function as an HTTP GET endpoint at /api/mapkeys. The auth_level=func.AuthLevel.ANONYMOUS setting means no API key is required to call it.

The function checks for an optional pillar query parameter. If provided, it filters the MAP Keys to only return those matching that revenue cycle pillar. Otherwise, it returns all 6.

  • Below the list_mapkeys function, paste the second endpoint that retrieves a single MAP Key by ID:


@app.route(route="mapkeys/{id}", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def get_mapkey(req: func.HttpRequest) -> func.HttpResponse:
    mapkey_id = req.route_params.get("id")
    logging.info(f"GET /api/mapkeys/{mapkey_id} - Getting single MAP Key")

    try:
        mapkey_id = int(mapkey_id)
    except (ValueError, TypeError):
        return func.HttpResponse(
            json.dumps({"error": "Invalid MAP Key ID"}),
            mimetype="application/json",
            status_code=400
        )

    mapkey = next((k for k in mapkeys if k["id"] == mapkey_id), None)

    if not mapkey:
        return func.HttpResponse(
            json.dumps({"error": "MAP Key not found"}),
            mimetype="application/json",
            status_code=404
        )

    # Include any submitted benchmarks for this MAP Key
    related_benchmarks = [b for b in benchmarks if b["mapkey_id"] == mapkey_id]
    result = {**mapkey, "submissions": related_benchmarks}

    return func.HttpResponse(
        json.dumps(result),
        mimetype="application/json",
        status_code=200
    )

What does this endpoint do?

This function handles requests to /api/mapkeys/{id} where {id} is a route parameter. It extracts the ID using req.route_params.get("id") and validates it is a number.

It returns the MAP Key details along with any benchmark submissions that reference it. The error handling returns a 400 for invalid IDs and a 404 if the MAP Key does not exist.

  • Below the get_mapkey function, paste the third endpoint that handles benchmark submissions:


@app.route(route="benchmarks", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def submit_benchmark(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("POST /api/benchmarks - Submitting benchmark data point")

    try:
        req_body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "Invalid JSON in request body"}),
            mimetype="application/json",
            status_code=400
        )

    required_fields = ["mapkey_id", "organization", "value", "period"]
    missing = [f for f in required_fields if f not in req_body]
    if missing:
        return func.HttpResponse(
            json.dumps({"error": f"Missing required fields: {', '.join(missing)}"}),
            mimetype="application/json",
            status_code=400
        )

    # Validate that the MAP Key exists
    mapkey = next((k for k in mapkeys if k["id"] == req_body["mapkey_id"]), None)
    if not mapkey:
        return func.HttpResponse(
            json.dumps({"error": f"MAP Key with ID {req_body['mapkey_id']} not found"}),
            mimetype="application/json",
            status_code=404
        )

    # Calculate a simple percentile ranking against the benchmark
    value = req_body["value"]
    median = mapkey["benchmark_median"]
    top_quartile = mapkey["benchmark_top_quartile"]

    if mapkey["direction"] == "higher_is_better":
        if value >= top_quartile:
            percentile = 75 + 25 * min((value - top_quartile) / (top_quartile - median + 1), 1)
        elif value >= median:
            percentile = 50 + 25 * (value - median) / (top_quartile - median)
        else:
            percentile = max(0, 50 * value / median)
    else:
        if value <= top_quartile:
            percentile = 75 + 25 * min((top_quartile - value) / (median - top_quartile + 1), 1)
        elif value <= median:
            percentile = 50 + 25 * (median - value) / (median - top_quartile)
        else:
            percentile = max(0, 50 * (1 - (value - median) / median))

    new_benchmark = {
        "id": len(benchmarks) + 1,
        "mapkey_id": req_body["mapkey_id"],
        "mapkey_name": mapkey["name"],
        "organization": req_body["organization"],
        "value": value,
        "period": req_body["period"],
        "percentile_rank": round(percentile, 1),
        "performance": "Above Median" if percentile >= 50 else "Below Median"
    }
    benchmarks.append(new_benchmark)

    return func.HttpResponse(
        json.dumps(new_benchmark),
        mimetype="application/json",
        status_code=201
    )

What does the percentile calculation do?

When an organization submits a benchmark value, the function compares it against the median and top quartile from the MAP Key definition. It accounts for directionality. For metrics where higher is better (like Pre-Registration Rate), a value above the top quartile scores above the 75th percentile. For metrics where lower is better (like Billing Lag Days), a value below the top quartile scores above the 75th percentile.

The response includes both a numeric percentile rank and a plain-English performance label (Above Median or Below Median). This mirrors how real benchmarking platforms present peer comparison results.

  • Save function_app.py.

Now verify that your requirements.txt has the correct dependency.

  • Open requirements.txt and confirm it contains the following line:
azure-functions==2.1.0

Why only one dependency?

The azure-functions package provides the decorator-based programming model (@app.route) and the request/response classes. Your API uses only the standard library (json, logging) for everything else, so no additional packages are needed.

  • Save requirements.txt if you made any changes.

✔️ Awesome, I've got everything!

Great. Double check you have saved both function_app.py and requirements.txt.

ⓧ I'd like to double check the full code

Here is the complete function_app.py file. Compare your code against this:

import azure.functions as func
import logging
import json

app = func.FunctionApp()

# In-memory MAP Key definitions organized by revenue cycle pillar
mapkeys = [
    {
        "id": 1,
        "name": "Pre-Registration Rate",
        "pillar": "Patient Access",
        "description": "Percentage of patients pre-registered before date of service",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 85.0,
        "benchmark_top_quartile": 95.0
    },
    {
        "id": 2,
        "name": "Point-of-Service Collections",
        "pillar": "Patient Access",
        "description": "Percentage of expected patient responsibility collected at time of service",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 32.0,
        "benchmark_top_quartile": 52.0
    },
    {
        "id": 3,
        "name": "Billing Lag Days",
        "pillar": "Pre-Billing",
        "description": "Average number of days from date of discharge to claim submission",
        "unit": "days",
        "direction": "lower_is_better",
        "benchmark_median": 5.5,
        "benchmark_top_quartile": 3.2
    },
    {
        "id": 4,
        "name": "Clean Claim Rate",
        "pillar": "Claims Adjudication",
        "description": "Percentage of claims accepted on first submission without edits or rejections",
        "unit": "percentage",
        "direction": "higher_is_better",
        "benchmark_median": 90.0,
        "benchmark_top_quartile": 96.0
    },
    {
        "id": 5,
        "name": "Net Days in A/R",
        "pillar": "Financial Management",
        "description": "Average number of days that net accounts receivable is outstanding",
        "unit": "days",
        "direction": "lower_is_better",
        "benchmark_median": 42.0,
        "benchmark_top_quartile": 33.0
    },
    {
        "id": 6,
        "name": "Denial Rate",
        "pillar": "Account Resolution",
        "description": "Percentage of claims denied by payers on initial submission",
        "unit": "percentage",
        "direction": "lower_is_better",
        "benchmark_median": 8.5,
        "benchmark_top_quartile": 4.0
    },
]

# In-memory benchmark submissions
benchmarks = []


@app.route(route="mapkeys", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def list_mapkeys(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("GET /api/mapkeys - Listing MAP Keys")

    pillar = req.params.get("pillar")
    if pillar:
        filtered = [k for k in mapkeys if k["pillar"].lower() == pillar.lower()]
        return func.HttpResponse(
            json.dumps(filtered),
            mimetype="application/json",
            status_code=200
        )

    return func.HttpResponse(
        json.dumps(mapkeys),
        mimetype="application/json",
        status_code=200
    )


@app.route(route="mapkeys/{id}", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def get_mapkey(req: func.HttpRequest) -> func.HttpResponse:
    mapkey_id = req.route_params.get("id")
    logging.info(f"GET /api/mapkeys/{mapkey_id} - Getting single MAP Key")

    try:
        mapkey_id = int(mapkey_id)
    except (ValueError, TypeError):
        return func.HttpResponse(
            json.dumps({"error": "Invalid MAP Key ID"}),
            mimetype="application/json",
            status_code=400
        )

    mapkey = next((k for k in mapkeys if k["id"] == mapkey_id), None)

    if not mapkey:
        return func.HttpResponse(
            json.dumps({"error": "MAP Key not found"}),
            mimetype="application/json",
            status_code=404
        )

    # Include any submitted benchmarks for this MAP Key
    related_benchmarks = [b for b in benchmarks if b["mapkey_id"] == mapkey_id]
    result = {**mapkey, "submissions": related_benchmarks}

    return func.HttpResponse(
        json.dumps(result),
        mimetype="application/json",
        status_code=200
    )


@app.route(route="benchmarks", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def submit_benchmark(req: func.HttpRequest) -> func.HttpResponse:
    logging.info("POST /api/benchmarks - Submitting benchmark data point")

    try:
        req_body = req.get_json()
    except ValueError:
        return func.HttpResponse(
            json.dumps({"error": "Invalid JSON in request body"}),
            mimetype="application/json",
            status_code=400
        )

    required_fields = ["mapkey_id", "organization", "value", "period"]
    missing = [f for f in required_fields if f not in req_body]
    if missing:
        return func.HttpResponse(
            json.dumps({"error": f"Missing required fields: {', '.join(missing)}"}),
            mimetype="application/json",
            status_code=400
        )

    # Validate that the MAP Key exists
    mapkey = next((k for k in mapkeys if k["id"] == req_body["mapkey_id"]), None)
    if not mapkey:
        return func.HttpResponse(
            json.dumps({"error": f"MAP Key with ID {req_body['mapkey_id']} not found"}),
            mimetype="application/json",
            status_code=404
        )

    # Calculate a simple percentile ranking against the benchmark
    value = req_body["value"]
    median = mapkey["benchmark_median"]
    top_quartile = mapkey["benchmark_top_quartile"]

    if mapkey["direction"] == "higher_is_better":
        if value >= top_quartile:
            percentile = 75 + 25 * min((value - top_quartile) / (top_quartile - median + 1), 1)
        elif value >= median:
            percentile = 50 + 25 * (value - median) / (top_quartile - median)
        else:
            percentile = max(0, 50 * value / median)
    else:
        if value <= top_quartile:
            percentile = 75 + 25 * min((top_quartile - value) / (median - top_quartile + 1), 1)
        elif value <= median:
            percentile = 50 + 25 * (median - value) / (median - top_quartile)
        else:
            percentile = max(0, 50 * (1 - (value - median) / median))

    new_benchmark = {
        "id": len(benchmarks) + 1,
        "mapkey_id": req_body["mapkey_id"],
        "mapkey_name": mapkey["name"],
        "organization": req_body["organization"],
        "value": value,
        "period": req_body["period"],
        "percentile_rank": round(percentile, 1),
        "performance": "Above Median" if percentile >= 50 else "Below Median"
    }
    benchmarks.append(new_benchmark)

    return func.HttpResponse(
        json.dumps(new_benchmark),
        mimetype="application/json",
        status_code=201
    )
Run and test your API locally

With all three endpoints implemented, you can now start the local Azure Functions host and verify that your API responds correctly to HTTP requests.

  • Start the local Functions runtime by pressing F5 in VS Code.

What happens when you press F5?

VS Code launches the Azure Functions host using func start behind the scenes. The terminal output lists your endpoint URLs. You should see three routes registered: list_mapkeys, get_mapkey, and submit_benchmark.

  • Open a browser and navigate to this URL to test the list endpoint:
http://localhost:7071/api/mapkeys
  • You should see a JSON array containing all 6 MAP Key definitions.
  • Test filtering by pillar by navigating to:
http://localhost:7071/api/mapkeys?pillar=Patient%20Access
  • You should see only 2 MAP Keys returned (Pre-Registration Rate and Point-of-Service Collections).
  • Test the single MAP Key endpoint by navigating to:
http://localhost:7071/api/mapkeys/1
  • You should see the Pre-Registration Rate MAP Key with an empty submissions array.
  • Test the POST endpoint by opening a new terminal in VS Code (Ctrl+Shift+`) and running this command:
curl -X POST http://localhost:7071/api/benchmarks -H "Content-Type: application/json" -d "{\"mapkey_id\": 1, \"organization\": \"General Hospital\", \"value\": 92.5, \"period\": \"2024-03\"}"
  • You should see a 201 response with JSON containing a percentile_rank value and a performance field showing "Above Median".

Why does 92.5 score above median?

The Pre-Registration Rate MAP Key has a median of 85.0 and a top quartile of 95.0. A value of 92.5 sits between the median and top quartile. Since the direction is higher_is_better, scoring above 85.0 means the organization is performing better than half of its peers.

Seeing an error when running func start?

If you see a message about the port already being in use, another process is occupying port 7071. Close any other terminals running func start and try again.

If you see a Python import error, make sure you are running within the virtual environment that the wizard created. Check that the VS Code terminal shows (.venv) at the start of the prompt line.

help me debug why my Azure Functions API won't start locally

Your API is running locally and all three endpoints work. Next, you will deploy this to Azure so it is accessible from anywhere on the internet.

Deploy to Azure

Your API is working locally, which proves the code is correct. Now it is time to make it accessible to the outside world by deploying it to Azure.

In this step, you will provision cloud resources, push your code to Azure, and verify that your live API responds correctly from a public URL.

In this step, get ready to:
  • Create a Function App and supporting resources in Azure.
  • Deploy your code to the cloud.
  • Test your live API from a public URL.
Create a Function App in Azure

A Function App is the Azure resource that hosts and runs your functions in the cloud. The VS Code extension provides a guided wizard that creates the Function App along with its supporting resources (a resource group and a storage account).

  • In VS Code, press F1 to open the command palette.
  • Type Azure Functions: Create Function App in Azure... (Advanced) and select it from the list.
  • Sign in to your Azure account when prompted.
  • Enter a globally unique name for your function app (e.g., revcycle-benchmark-yourname).
  • Select Python 3.11 as the runtime stack.
  • Select Consumption as the hosting plan.
  • Select a region close to you (e.g., East US).
  • For the resource group prompt, select Create new and enter rg-revcycle-benchmark.
  • For the storage account prompt, select Create new and accept the generated name.
  • Wait for Azure to provision the resources. This takes 1-2 minutes. You can watch progress in the Azure: Activity Log panel.

Why the Consumption plan?

The Consumption plan is Azure's serverless hosting option. You only pay when your function executes, and the first 1 million requests per month are free.

For a benchmarking query service with variable traffic (quiet most of the month, busy during submission windows), this is the most cost-effective choice.

Not seeing the Advanced option?

Make sure you are typing the full command including "(Advanced)" at the end. The basic version creates resources with default names. The Advanced version lets you name your resource group and storage account.

If the Azure sign-in prompt does not appear, open the command palette again and search for Azure: Sign In to authenticate manually.

help me sign in to Azure from VS Code

Deploy your code to Azure

With the Function App provisioned, you can now push your local project code up to Azure. The VS Code extension packages your code and deploys it using a remote build process.

  • Press F1 to open the command palette.
  • Type Azure Functions: Deploy to Function App... and select it.
  • Select the function app you just created from the list.
  • When prompted about overwriting existing content, click Deploy to confirm.
  • Wait for the deployment to complete. VS Code shows a notification when it finishes.

What happens during deployment?

The extension packages your project files and uploads them to Azure. Azure then performs a remote build, which installs the dependencies from requirements.txt and starts your function app.

Deployment failed?

Check the Output panel in VS Code (select Azure Functions from the dropdown) for specific error messages. Common causes include selecting the wrong function app or network timeouts.

If you see a Python version mismatch error, verify that you selected Python 3.11 as the runtime stack when creating the Function App.

help me fix my Azure Functions deployment error

Test your live API

Your API is now live on the internet. Time to confirm that each endpoint returns the correct responses from the Azure-hosted version.

  • Press F1 and type Azure Functions: Copy Function URL.
  • Select your function app, then select list_mapkeys.
  • Paste the URL in your browser. You should see the full JSON list of MAP Key definitions.
  • Test the single MAP Key endpoint by modifying the URL path to end in /api/mapkeys/1. You should see the Pre-Registration Rate KPI details.
  • Copy your live API base URL. This is what you would share with frontend teams or partner integrations.

What about the POST endpoint?

The POST /benchmarks endpoint works from the live URL too, but browsers can only send GET requests via the address bar. You can test it using the same curl command from earlier, replacing localhost:7071 with your live Azure domain.

Seeing an error or empty response?

If you get a 404, double-check the URL path includes /api/mapkeys (not just the root domain).

If you see a generic Azure error page, the deployment might still be starting up. Wait 30 seconds and refresh. The Consumption plan can have a brief cold start on the first request after deployment.

help me troubleshoot my Azure Functions API returning errors

Cost warning

Azure Functions Consumption plan is free for up to 1 million requests per month. Your testing will use a negligible fraction of this allowance. No charges will occur from normal project use.

Your benchmarking API is live and accessible to the world. Next up, you will write an Architecture Decision Record that documents why you made the infrastructure choices you just implemented.

Write Your Architecture Decision Record

Your API is live on Azure and serving real revenue cycle KPI data to anyone with the URL. That is the working infrastructure.

Now you will produce the professional artifact that proves you can reason about why you chose this infrastructure. An Architecture Decision Record (ADR) captures what was decided, what alternatives were considered, and what trade-offs were accepted. This is the document format that Solution Architects produce daily at companies like Google and Amazon.

In this step, get ready to:
  • Create an ADR document recording three architectural decisions.
  • Review the trade-offs for compute platform, data layer, and authentication.
  • Personalize the document with your own domain observations.
Create the ADR document

The ADR format was introduced by Michael Nygard in 2011. Each record is a short Markdown file that captures a single significant decision alongside its context, alternatives, and consequences.

  • In VS Code, click on the Explorer sidebar to see your RevCycleBenchmarkAPI folder.
  • Right-click in the file list and select New File.
  • Name the file ADR.md and press Enter.
  • Paste the following ADR content into the file:
# Architecture Decision Record: Revenue Cycle Benchmarking API

## Status

Accepted

## Context

Our organization operates a healthcare revenue cycle benchmarking platform (similar to HFMA's MAP App) that enables hospitals and health systems to submit monthly financial and operational data, measure performance against standardized KPIs (MAP Keys), and benchmark against 600+ peer organizations. The platform currently runs as a monolithic ASP.NET Core application on Azure App Service.

We are evaluating whether to decompose the KPI query/benchmarking service into a standalone microservice. This API would serve MAP Key definitions, accept benchmark submissions, and calculate percentile rankings. It would be consumed by the Angular frontend dashboard, the peer group comparison reports, and potentially by partner health system integrations.

### Requirements

- RESTful API with JSON responses serving KPI definitions and benchmark data
- Low latency for read operations (under 200ms p95 for MAP Key lookups, as dashboards load multiple KPIs)
- Handle periodic traffic spikes during monthly data submission windows (first week of each month)
- Low operational overhead (CareCloud in-house team manages the platform without dedicated DevOps)
- Cost-effective for variable traffic (quiet most of the month, busy during submission windows)
- Must deploy within the Azure ecosystem (organizational standard)
- Data sensitivity: healthcare financial data requires appropriate access controls

## Decision 1: Compute Platform

### Options Considered

| Criteria | Azure Functions (Consumption) | Azure App Service (existing) | Azure Container Apps |
|----------|-------------------------------|------------------------------|---------------------|
| Cold start | Yes (1-3s first request) | None (always running) | Yes (shorter) |
| Cost at current traffic | ~$0/month (free tier covers KPI lookups) | Already paying ~$50/month for monolith | ~$0/month (scale to zero) |
| Scaling during submission windows | Automatic, per-request | Manual or auto-scale rules (already configured) | Automatic, per-request |
| Operational overhead | Minimal (no patching, no capacity planning) | Moderate (already managed by team) | Low-moderate (container image management) |
| Separation from monolith | Clean boundary, independent deploy | Adds complexity within existing codebase | Clean boundary, independent deploy |
| Language flexibility | Python (enables data team involvement) | C# (matches existing stack) | Any language |

### Decision

Azure Functions on the Consumption plan for the benchmarking query microservice.

### Rationale

- Traffic pattern fits perfectly: the KPI lookup service is read-heavy with predictable spikes during submission windows. Serverless scales automatically during those windows and costs nothing during quiet periods.
- Clean separation from the ASP.NET Core monolith without requiring the team to maintain a second App Service instance.
- Python enables the data analytics team to contribute to percentile calculations and statistical logic without learning C#.
- At current query volume (under 50K requests/day even during peaks), we stay within the free tier.
- If the team decides the cold start is unacceptable for dashboard load times, migration to the Premium plan (with pre-warmed instances) requires zero code changes.

### Trade-off Accepted

Cold starts of 1-3 seconds on first request after idle period. Mitigation: dashboard UX already shows loading states. If user research indicates this latency is problematic, enable "Always Ready" instances on Premium plan (~$0.018/vCPU-s). For a single instance, this adds roughly $50/month.

## Decision 2: Data Layer

### Options Considered

| Criteria | In-Memory (current MVP) | Azure SQL Database | Azure Cosmos DB | Azure Table Storage |
|----------|-------------------------|-------------------|-----------------|-------------------|
| Persistence | None (resets on restart) | Full ACID | Full, multi-region | Full, eventual consistency |
| Cost | $0 | ~$5/month (serverless) | ~$0 (free tier: 1000 RU/s) | ~$0 (minimal) |
| Percentile calculations | Python-native | T-SQL window functions (PERCENTILE_CONT) | Must compute in app layer | Must compute in app layer |
| Peer group joins | Python filtering | Full SQL JOIN (organization type, bed count, region) | Requires denormalization | Not supported |
| Schema enforcement | None | Strong (financial data integrity) | Schema-free | Schema-free |
| Team expertise | N/A | Strong (existing team skill) | Minimal | Minimal |

### Decision

In-memory for MVP. Migrate to Azure SQL Database for production.

### Rationale

- In-memory allows rapid prototyping and proves the API contract works end-to-end.
- Azure SQL Database is the production target because:
  - Percentile calculations are natively supported via T-SQL window functions (PERCENTILE_CONT, NTILE). This is critical for benchmarking and avoids reimplementing statistical logic in application code.
  - Peer group comparisons require complex JOINs across organization attributes (bed count ranges, geographic region, system membership). SQL handles this naturally.
  - Healthcare financial data requires strong schema enforcement and ACID transactions to prevent data quality issues that would undermine benchmark credibility.
  - The CareCloud team already has deep SQL expertise from the existing MAP App database.
- Cosmos DB was rejected because: benchmarking data is fundamentally relational (organizations belong to peer groups, KPIs belong to pillars, submissions reference both). The partition key design for cross-organization percentile calculations would require expensive cross-partition queries, negating Cosmos DB's performance advantage.

### Migration Path

1. Add pyodbc to requirements.txt
2. Replace in-memory list with parameterized SQL queries
3. Add connection string to Azure Functions application settings
4. Implement PERCENTILE_CONT queries for real peer benchmarking
5. No API contract changes needed (OpenAPI spec stays the same)

## Decision 3: Authentication and Data Security

### Decision

Anonymous access for MVP. Azure API Management with OAuth 2.0 for production.

### Rationale

- Anonymous allows easy testing and development during the prototype phase.
- Production requires OAuth 2.0 because:
  - Benchmark data is sensitive healthcare financial information. Organizations submit A/R days, denial rates, and collection figures that are competitive intelligence.
  - Each organization should only see their own submission data alongside anonymized peer comparisons.
  - Azure API Management provides rate limiting, API key management, and OAuth integration in a single layer.
  - HIPAA compliance: while MAP Key data itself is not PHI, the financial benchmarks could be considered business-sensitive under BAA terms.

### Upgrade Path

1. MVP: Anonymous (current state for development)
2. Phase 1: Function-level API keys (simple, immediate security)
3. Phase 2: Azure API Management with OAuth 2.0, per-organization scoping, and rate limiting

## Consequences

- The benchmarking query service deploys independently from the main ASP.NET Core platform, enabling faster iteration on KPI logic.
- Cold starts are a known limitation but acceptable for dashboard loading patterns (users tolerate brief loading states).
- Data resets on function restart in the MVP (acceptable for proving the contract, addressed in migration path).
- The OpenAPI specification serves as the stable API contract: frontend teams can develop against it regardless of backend implementation changes.
- Python runtime enables the data team to own percentile calculation logic without depending on the C# backend team.

## Review Date

Re-evaluate compute platform after 3 months of production traffic data. Key triggers: cold start complaints from users, submission window response times exceeding 500ms p95, or monthly request volume approaching 500K.

What is an Architecture Decision Record?

An ADR is a short document that records a single significant architectural decision. The format was popularized by Michael Nygard in 2011 and is now the industry standard at companies that practice architecture governance.

Each ADR follows a consistent structure: Status, Context (the forces at play), Decision (what was chosen), and Consequences (what becomes easier or harder). Your ADR extends this with comparison tables and migration paths, which is common in practice.

  • Save ADR.md.
  • Confirm the file appears in your Explorer sidebar alongside function_app.py and openapi.yaml.
Understand the architectural decisions you made

Read through your ADR carefully. Each decision section records a real trade-off you navigated when building this API.

Why Azure Functions over App Service?

Your existing platform runs as a monolithic ASP.NET Core application on Azure App Service. The ADR positions the benchmarking API as a microservice decomposition from that monolith.

The traffic pattern is the key factor. KPI lookups are read-heavy with predictable spikes during monthly submission windows. Azure Functions on the Consumption plan scales automatically during those windows and costs nothing during quiet periods.

Why SQL over Cosmos DB for benchmarking data?

Benchmarking data is fundamentally relational. Organizations belong to peer groups, KPIs belong to pillars, and submissions reference both.

Azure SQL Database supports PERCENTILE_CONT natively via T-SQL window functions. This is critical for peer benchmarking without reimplementing statistical logic in application code. Cosmos DB would require expensive cross-partition queries for cross-organization percentile calculations.

Why anonymous auth for the MVP?

Anonymous access enables easy testing during prototyping. The ADR documents the upgrade path: function-level API keys first, then Azure API Management with OAuth 2.0 for production.

This is critical because benchmark data is sensitive healthcare financial information. Organizations submit A/R days, denial rates, and collection figures that are competitive intelligence.

Make the ADR your own

A generic ADR is useful as a template. A personalized ADR demonstrates that you can apply architectural reasoning to your actual domain.

  • Scroll to the Context section in ADR.md.
  • Edit the paragraph starting with "We are evaluating..." to reference a real challenge you have observed with the platform's architecture (for example, monolith scaling, report generation latency, or data submission bottlenecks).
  • Scroll to the bottom of the file, below the Review Date section.
  • Add a new section titled ## Lessons Learned with 2-3 observations from building this project.

Strong Lessons Learned entries

Good observations connect the hands-on experience to a broader principle. For example: what surprised you about the deployment process, what you would change about the data model for production, or what the cold start experience taught you about user expectations.

  • Save ADR.md.

Not sure what to write?

Your Lessons Learned section is personal reflection. There is no wrong answer. Think about what you would tell a teammate who is about to build a similar project.

help me write Lessons Learned entries for my ADR based on deploying an Azure Functions API

✔️ Awesome, I've got everything!

Great. Double check that ADR.md is saved with your personalized Context and Lessons Learned sections.

ⓧ I'd like to double check the full code

Here is the complete ADR.md template before personalization. Your version will differ in the Context paragraph and will include an additional Lessons Learned section at the bottom.

# Architecture Decision Record: Revenue Cycle Benchmarking API

## Status

Accepted

## Context

Our organization operates a healthcare revenue cycle benchmarking platform (similar to HFMA's MAP App) that enables hospitals and health systems to submit monthly financial and operational data, measure performance against standardized KPIs (MAP Keys), and benchmark against 600+ peer organizations. The platform currently runs as a monolithic ASP.NET Core application on Azure App Service.

We are evaluating whether to decompose the KPI query/benchmarking service into a standalone microservice. This API would serve MAP Key definitions, accept benchmark submissions, and calculate percentile rankings. It would be consumed by the Angular frontend dashboard, the peer group comparison reports, and potentially by partner health system integrations.

### Requirements

- RESTful API with JSON responses serving KPI definitions and benchmark data
- Low latency for read operations (under 200ms p95 for MAP Key lookups, as dashboards load multiple KPIs)
- Handle periodic traffic spikes during monthly data submission windows (first week of each month)
- Low operational overhead (CareCloud in-house team manages the platform without dedicated DevOps)
- Cost-effective for variable traffic (quiet most of the month, busy during submission windows)
- Must deploy within the Azure ecosystem (organizational standard)
- Data sensitivity: healthcare financial data requires appropriate access controls

## Decision 1: Compute Platform

### Options Considered

| Criteria | Azure Functions (Consumption) | Azure App Service (existing) | Azure Container Apps |
|----------|-------------------------------|------------------------------|---------------------|
| Cold start | Yes (1-3s first request) | None (always running) | Yes (shorter) |
| Cost at current traffic | ~$0/month (free tier covers KPI lookups) | Already paying ~$50/month for monolith | ~$0/month (scale to zero) |
| Scaling during submission windows | Automatic, per-request | Manual or auto-scale rules (already configured) | Automatic, per-request |
| Operational overhead | Minimal (no patching, no capacity planning) | Moderate (already managed by team) | Low-moderate (container image management) |
| Separation from monolith | Clean boundary, independent deploy | Adds complexity within existing codebase | Clean boundary, independent deploy |
| Language flexibility | Python (enables data team involvement) | C# (matches existing stack) | Any language |

### Decision

Azure Functions on the Consumption plan for the benchmarking query microservice.

### Rationale

- Traffic pattern fits perfectly: the KPI lookup service is read-heavy with predictable spikes during submission windows. Serverless scales automatically during those windows and costs nothing during quiet periods.
- Clean separation from the ASP.NET Core monolith without requiring the team to maintain a second App Service instance.
- Python enables the data analytics team to contribute to percentile calculations and statistical logic without learning C#.
- At current query volume (under 50K requests/day even during peaks), we stay within the free tier.
- If the team decides the cold start is unacceptable for dashboard load times, migration to the Premium plan (with pre-warmed instances) requires zero code changes.

### Trade-off Accepted

Cold starts of 1-3 seconds on first request after idle period. Mitigation: dashboard UX already shows loading states. If user research indicates this latency is problematic, enable "Always Ready" instances on Premium plan (~$0.018/vCPU-s). For a single instance, this adds roughly $50/month.

## Decision 2: Data Layer

### Options Considered

| Criteria | In-Memory (current MVP) | Azure SQL Database | Azure Cosmos DB | Azure Table Storage |
|----------|-------------------------|-------------------|-----------------|-------------------|
| Persistence | None (resets on restart) | Full ACID | Full, multi-region | Full, eventual consistency |
| Cost | $0 | ~$5/month (serverless) | ~$0 (free tier: 1000 RU/s) | ~$0 (minimal) |
| Percentile calculations | Python-native | T-SQL window functions (PERCENTILE_CONT) | Must compute in app layer | Must compute in app layer |
| Peer group joins | Python filtering | Full SQL JOIN (organization type, bed count, region) | Requires denormalization | Not supported |
| Schema enforcement | None | Strong (financial data integrity) | Schema-free | Schema-free |
| Team expertise | N/A | Strong (existing team skill) | Minimal | Minimal |

### Decision

In-memory for MVP. Migrate to Azure SQL Database for production.

### Rationale

- In-memory allows rapid prototyping and proves the API contract works end-to-end.
- Azure SQL Database is the production target because:
  - Percentile calculations are natively supported via T-SQL window functions (PERCENTILE_CONT, NTILE). This is critical for benchmarking and avoids reimplementing statistical logic in application code.
  - Peer group comparisons require complex JOINs across organization attributes (bed count ranges, geographic region, system membership). SQL handles this naturally.
  - Healthcare financial data requires strong schema enforcement and ACID transactions to prevent data quality issues that would undermine benchmark credibility.
  - The CareCloud team already has deep SQL expertise from the existing MAP App database.
- Cosmos DB was rejected because: benchmarking data is fundamentally relational (organizations belong to peer groups, KPIs belong to pillars, submissions reference both). The partition key design for cross-organization percentile calculations would require expensive cross-partition queries, negating Cosmos DB's performance advantage.

### Migration Path

1. Add pyodbc to requirements.txt
2. Replace in-memory list with parameterized SQL queries
3. Add connection string to Azure Functions application settings
4. Implement PERCENTILE_CONT queries for real peer benchmarking
5. No API contract changes needed (OpenAPI spec stays the same)

## Decision 3: Authentication and Data Security

### Decision

Anonymous access for MVP. Azure API Management with OAuth 2.0 for production.

### Rationale

- Anonymous allows easy testing and development during the prototype phase.
- Production requires OAuth 2.0 because:
  - Benchmark data is sensitive healthcare financial information. Organizations submit A/R days, denial rates, and collection figures that are competitive intelligence.
  - Each organization should only see their own submission data alongside anonymized peer comparisons.
  - Azure API Management provides rate limiting, API key management, and OAuth integration in a single layer.
  - HIPAA compliance: while MAP Key data itself is not PHI, the financial benchmarks could be considered business-sensitive under BAA terms.

### Upgrade Path

1. MVP: Anonymous (current state for development)
2. Phase 1: Function-level API keys (simple, immediate security)
3. Phase 2: Azure API Management with OAuth 2.0, per-organization scoping, and rate limiting

## Consequences

- The benchmarking query service deploys independently from the main ASP.NET Core platform, enabling faster iteration on KPI logic.
- Cold starts are a known limitation but acceptable for dashboard loading patterns (users tolerate brief loading states).
- Data resets on function restart in the MVP (acceptable for proving the contract, addressed in migration path).
- The OpenAPI specification serves as the stable API contract: frontend teams can develop against it regardless of backend implementation changes.
- Python runtime enables the data team to own percentile calculation logic without depending on the C# backend team.

## Review Date

Re-evaluate compute platform after 3 months of production traffic data. Key triggers: cold start complaints from users, submission window response times exceeding 500ms p95, or monthly request volume approaching 500K.

Secret mission

Connect Azure DevOps CI/CD Pipeline

Your API is live, but every change requires a manual deploy from VS Code. Real engineering teams automate this. Connect your project to Azure DevOps and set up a CI/CD pipeline that auto-deploys your API every time you push code to the main branch.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The Azure Functions Consumption plan only charges per execution, and the free tier covers 1 million requests per month.

Cost warning

The Azure Storage Account created alongside your Function App costs a few pennies per month for storing function metadata. While negligible, deleting the resource group removes this cost entirely.

Resources you used:

  • Azure Function App (Consumption plan) in resource group rg-revcycle-benchmark.
  • Azure Storage Account (auto-created with the Function App) in resource group rg-revcycle-benchmark.
  • Azure DevOps repository and CI/CD pipeline (if you completed the Secret Mission).
  • Local project folder RevCycleBenchmarkAPI on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend the project further.

  • Your Function App is on the Consumption plan, which means you only pay when the function executes. The free tier covers 1 million requests per month.
  • The Storage Account will cost a few pennies per month for function metadata. No other charges accrue.
  • Your Azure DevOps repository and pipeline remain available at no cost.
  • Your local RevCycleBenchmarkAPI folder stays on your machine for future reference.

Pause - I'll come back to this later

Stop the Function App so no requests can trigger executions. Your code and configuration remain intact in Azure for when you return.

  • In the Azure portal, navigate to Resource groups and select rg-revcycle-benchmark.
  • Select your Function App from the resource list.
  • Click Stop in the top toolbar to prevent any executions.
  • When you are ready to resume, return to the same page and click Start.

Delete - I don't want to use this again

Remove all project resources and start fresh. This is irreversible.

Delete Azure resources

Deleting the resource group removes the Function App, Storage Account, and all associated resources in one action.

  • In the Azure portal, select Resource groups from the left-hand menu.
  • Select rg-revcycle-benchmark.
  • Click Delete resource group at the top of the page.
  • Type rg-revcycle-benchmark to confirm, then click Delete.
Delete Azure DevOps repository and pipeline (Secret Mission only)

If you completed the Secret Mission, remove the repository and pipeline from Azure DevOps.

  • In Azure DevOps, go to Project settings.
  • Select Repositories and find your repository in the list.
  • Click the ellipsis (...) next to your repository, then select Delete.
  • Confirm deletion by typing the repository name and clicking Delete.

The associated pipeline is removed automatically when the repository is deleted.

Delete local project files
  • Delete your local project folder by running the appropriate command for your operating system.

On macOS or Linux:

cd ~/Desktop
rm -rf RevCycleBenchmarkAPI

On Windows (PowerShell):

cd ~\Desktop
Remove-Item -Recurse -Force RevCycleBenchmarkAPI

Nice Work!

Nice Work!

You just designed, built, and deployed a revenue cycle benchmarking API on Azure Functions. You also produced professional architectural documentation that proves you can make and defend infrastructure decisions in a healthcare data context.

You learned how to:

  • Build and deploy a live REST API on Azure Functions using Python, serving revenue cycle KPI benchmarking data with filtering, detail retrieval, and data submission endpoints.
  • Author a professional OpenAPI 3.0 specification defining the API contract for MAP Key data access, including request parameters, response schemas, and error handling.
  • Write an Architecture Decision Record comparing compute platforms and data layer options, documenting trade-offs in the context of healthcare financial data.
  • Secret Mission: Connected your project to Azure DevOps CI/CD for automated deployments on every push to your main branch.

Ready to quiz yourself?