Back to Blog

Pricing Service: Node.js Architecture Deep Dive

Comprehensive technical documentation of production codebase architectures

ArchitectureDocumentationTypeScriptC# .NETNext.jsNode.js
šŸ“š Comprehensive architecture guides

Pricing Service: Node.js Architecture Deep Dive

Executive Summary

The pricing-service is a serverless GraphQL API built with TypeScript that runs entirely on the Node.js runtime within AWS Lambda functions. This document provides a comprehensive technical explanation of how Node.js is used throughout the architecture, from GraphQL resolvers to event-driven processing.


Table of Contents

  1. Node.js Runtime Foundation
  2. TypeScript to Node.js Compilation
  3. GraphQL API with AWS AppSync
  4. Lambda Functions on Node.js Runtime
  5. Event-Driven Architecture
  6. Code Structure and Patterns
  7. Deployment and Execution
  8. Performance Optimization
  9. Development Workflow

Node.js Runtime Foundation

What is Node.js in This Context?

Node.js is the JavaScript runtime environment that executes all the TypeScript code in the pricing-service. While the code is written in TypeScript, it compiles to JavaScript and runs on Node.js.

Why Node.js?

  1. Serverless Native: AWS Lambda natively supports Node.js runtime (versions 14.x, 16.x, 18.x, 20.x)
  2. TypeScript Compatibility: TypeScript compiles to JavaScript, which Node.js executes
  3. Event-Driven: Node.js's non-blocking I/O model is perfect for serverless, event-driven architectures
  4. GraphQL Ecosystem: Rich ecosystem of GraphQL libraries for Node.js
  5. Performance: Single-digit millisecond latency requirements met with Node.js's efficient execution

Runtime Environment

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   AWS Lambda Execution Environment  │
│                                     │
│   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│   │   Node.js Runtime (18.x)     │   │
│   │                             │   │
│   │   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │   │
│   │   │ Compiled JavaScript   │ │   │
│   │   │ (from TypeScript)      │ │   │
│   │   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │   │
│   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

TypeScript to Node.js Compilation

Compilation Process

The pricing-service is written in TypeScript but executes as JavaScript on Node.js:

// Source: pricing-service/src/resolvers/pricing.ts (TypeScript)
export const getPrice = async (
  parent: any,
  args: { storeId: string; productId: string },
  context: AppSyncContext
): Promise<Price> => {
  const { storeId, productId } = args;
  const price = await dynamoDB.get({
    TableName: 'prices',
    Key: { storeId, productId }
  });
  return price.Item as Price;
};

Compilation Step:

tsc --target ES2020 --module commonjs --outDir dist

Result (JavaScript for Node.js):

// Compiled: dist/resolvers/pricing.js (JavaScript)
exports.getPrice = async (parent, args, context) => {
  const { storeId, productId } = args;
  const price = await dynamoDB.get({
    TableName: 'prices',
    Key: { storeId, productId }
  });
  return price.Item;
};

TypeScript Configuration

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",           // Node.js 14+ compatible
    "module": "commonjs",          // Node.js module system
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Node.js Module System

The compiled JavaScript uses CommonJS modules (Node.js standard):

// CommonJS exports (Node.js)
module.exports = { getPrice };
// or
exports.getPrice = getPrice;

// CommonJS imports (Node.js)
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const dynamoDB = require('./dynamoDB');

GraphQL API with AWS AppSync

AWS AppSync Architecture

AWS AppSync is a managed GraphQL service that uses Node.js Lambda functions as resolvers:

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│           Client Request (GraphQL)               │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                   │
                   ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│            AWS AppSync (Managed)                 │
│  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│  │  GraphQL Schema Definition               │   │
│  │  - Query: getPrice, listPrices           │   │
│  │  - Mutation: updatePrice, createPrice    │   │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
│                   │                              │
│                   ā–¼                              │
│  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│  │  Resolver Mapping                        │   │
│  │  Query.getPrice → Lambda Function        │   │
│  │  Mutation.updatePrice → Lambda Function   │   │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                   │
                   ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│     AWS Lambda (Node.js Runtime)                │
│  ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”   │
│  │  TypeScript Resolver Code                │   │
│  │  (Compiled to JavaScript)                │   │
│  │                                           │   │
│  │  exports.handler = async (event) => {     │   │
│  │    // GraphQL resolver logic             │   │
│  │    return await getPrice(event);         │   │
│  │  };                                       │   │
│  ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜   │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
                   │
                   ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│              DynamoDB                            │
│  - prices table                                 │
│  - store_info table                              │
│  - price_changes table                           │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

GraphQL Schema Definition

# schema.graphql
type Query {
  getPrice(storeId: ID!, productId: ID!): Price
  listPrices(storeId: ID!, filter: PriceFilter): [Price]
}

type Mutation {
  updatePrice(input: UpdatePriceInput!): Price
  createPrice(input: CreatePriceInput!): Price
}

type Price {
  storeId: ID!
  productId: ID!
  amount: Float!
  currency: String!
  effectiveDate: String!
  expirationDate: String
}

Resolver Implementation (Node.js)

// src/resolvers/pricing.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
import type { AppSyncResolverEvent } from 'aws-lambda';

const client = new DynamoDBClient({ region: 'us-east-1' });
const docClient = DynamoDBDocumentClient.from(client);

export const getPrice = async (
  event: AppSyncResolverEvent<{ storeId: string; productId: string }>
) => {
  const { storeId, productId } = event.arguments;
  
  // Node.js async/await for non-blocking I/O
  const result = await docClient.send(
    new GetCommand({
      TableName: process.env.PRICES_TABLE!,
      Key: { storeId, productId }
    })
  );
  
  return result.Item;
};

// Lambda handler (Node.js entry point)
export const handler = async (event: AppSyncResolverEvent) => {
  const { fieldName, arguments: args } = event.info;
  
  switch (fieldName) {
    case 'getPrice':
      return await getPrice(event);
    case 'listPrices':
      return await listPrices(event);
    case 'updatePrice':
      return await updatePrice(event);
    default:
      throw new Error(`Unknown field: ${fieldName}`);
  }
};

AppSync Resolver Configuration

// infrastructure/appsync-resolver.ts (AWS CDK)
import * as appsync from 'aws-cdk-lib/aws-appsync';
import * as lambda from 'aws-cdk-lib/aws-lambda';

const resolverFunction = new lambda.Function(this, 'PricingResolver', {
  runtime: lambda.Runtime.NODEJS_18_X,  // Node.js 18 runtime
  handler: 'index.handler',              // Node.js entry point
  code: lambda.Code.fromAsset('dist'),   // Compiled JavaScript
  environment: {
    PRICES_TABLE: pricesTable.tableName,
    NODE_ENV: 'production'
  }
});

const api = new appsync.GraphqlApi(this, 'PricingApi', {
  name: 'pricing-service',
  schema: appsync.Schema.fromAsset('schema.graphql'),
  authorizationConfig: {
    defaultAuthorization: {
      authorizationType: appsync.AuthorizationType.USER_POOL,
      userPoolConfig: {
        userPool: cognitoUserPool
      }
    }
  }
});

// Attach Lambda resolver to GraphQL field
const getPriceDataSource = api.addLambdaDataSource(
  'getPriceDataSource',
  resolverFunction
);

getPriceDataSource.createResolver({
  typeName: 'Query',
  fieldName: 'getPrice'
});

Lambda Functions on Node.js Runtime

Lambda Function Structure

Each GraphQL resolver and event handler is a Node.js Lambda function:

// src/lambda/pricing-resolver/index.ts
import type { AppSyncResolverEvent, Context } from 'aws-lambda';

// Node.js Lambda handler signature
export const handler = async (
  event: AppSyncResolverEvent,
  context: Context
): Promise<any> => {
  // Node.js execution context
  console.log('Event:', JSON.stringify(event, null, 2));
  console.log('Context:', context.requestId);
  
  try {
    // Resolver logic
    const result = await resolveQuery(event);
    return result;
  } catch (error) {
    console.error('Error:', error);
    throw error;
  }
};

Lambda Runtime Configuration

// infrastructure/lambda-stack.ts (AWS CDK)
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class PricingServiceStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // Node.js 18.x runtime
    const resolverLambda = new lambda.Function(this, 'PricingResolver', {
      runtime: lambda.Runtime.NODEJS_18_X,  // Node.js runtime
      handler: 'index.handler',               // handler.exportedFunction
      code: lambda.Code.fromAsset('dist'),    // Compiled JavaScript
      timeout: Duration.seconds(30),         // Node.js execution timeout
      memorySize: 512,                        // Memory allocation
      environment: {
        NODE_ENV: 'production',
        PRICES_TABLE: pricesTable.tableName,
        STORE_INFO_TABLE: storeInfoTable.tableName
      }
    });
  }
}

Node.js Execution Model

Lambda Invocation Flow:
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  1. Request arrives at AppSync     │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  2. AppSync invokes Lambda          │
│     (Node.js runtime starts)         │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  3. Node.js loads handler module    │
│     - require('./index.js')         │
│     - Execute handler function      │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  4. Handler executes (async)        │
│     - await DynamoDB query          │
│     - Process data                  │
│     - Return result                 │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│  5. Result returned to AppSync      │
│     - Node.js runtime may persist   │
│       for warm start                 │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Cold Start vs Warm Start

Cold Start (Node.js initialization):

  1. Lambda service starts Node.js runtime
  2. Loads handler module (require('./index.js'))
  3. Executes handler function
  4. Time: ~100-500ms (first invocation)

Warm Start (Node.js already loaded):

  1. Reuses existing Node.js runtime
  2. Handler module already in memory
  3. Executes handler function immediately
  4. Time: <10ms (subsequent invocations)

Optimization for Single-Digit Millisecond Latency:

  • Provisioned concurrency for critical paths
  • Module caching in Node.js
  • Connection pooling for DynamoDB
  • Minimal dependencies to reduce cold start

Event-Driven Architecture

DynamoDB Streams + Lambda (Node.js)

The pricing-service uses DynamoDB Streams to trigger Node.js Lambda functions for event processing:

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│      DynamoDB Table (prices)        │
│  - Price update written              │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│      DynamoDB Streams               │
│  - Captures table changes           │
│  - Streams events to Lambda          │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   Lambda Function (Node.js)         │
│   ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”  │
│   │  exports.handler = async     │  │
│   │    (event) => {              │  │
│   │    // Process stream record   │  │
│   │    // Schedule price change   │  │
│   │    // Update store info       │  │
│   │  }                            │  │
│   ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜  │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜
               │
               ā–¼
ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│   Event Processing                  │
│  - Schedule price changes           │
│  - Update 8,000+ stores             │
│  - Send notifications               │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Stream Handler Implementation (Node.js)

// src/lambda/stream-processor/index.ts
import type { DynamoDBStreamEvent, Context } from 'aws-lambda';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBStreamsClient } from '@aws-sdk/client-dynamodb-streams';

export const handler = async (
  event: DynamoDBStreamEvent,
  context: Context
): Promise<void> => {
  // Node.js processes stream records asynchronously
  for (const record of event.Records) {
    if (record.eventName === 'INSERT' || record.eventName === 'MODIFY') {
      const newImage = record.dynamodb?.NewImage;
      
      if (newImage) {
        // Process price change event
        await processPriceChange(newImage);
        
        // Schedule price update for stores
        await schedulePriceUpdate(newImage);
      }
    }
  }
};

async function processPriceChange(priceRecord: any) {
  // Node.js async I/O for DynamoDB operations
  const storeId = priceRecord.storeId.S;
  const productId = priceRecord.productId.S;
  const newPrice = parseFloat(priceRecord.amount.N);
  
  // Update store pricing cache
  await updateStoreCache(storeId, productId, newPrice);
  
  // Trigger notifications
  await sendPriceChangeNotification(storeId, productId, newPrice);
}

async function schedulePriceUpdate(priceRecord: any) {
  // Node.js event scheduling
  const effectiveDate = new Date(priceRecord.effectiveDate.S);
  const now = new Date();
  
  if (effectiveDate > now) {
    // Schedule future price change
    await scheduleLambdaInvocation(effectiveDate, priceRecord);
  } else {
    // Apply immediately
    await applyPriceChange(priceRecord);
  }
}

Event-Driven Lambda Configuration

// infrastructure/stream-processor.ts (AWS CDK)
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

const streamProcessor = new lambda.Function(this, 'StreamProcessor', {
  runtime: lambda.Runtime.NODEJS_18_X,  // Node.js runtime
  handler: 'index.handler',
  code: lambda.Code.fromAsset('dist/stream-processor'),
  timeout: Duration.minutes(5),
  memorySize: 1024,
  environment: {
    STORE_INFO_TABLE: storeInfoTable.tableName,
    NOTIFICATION_TOPIC: notificationTopic.topicArn
  }
});

// Enable DynamoDB Streams
pricesTable.enableStream(dynamodb.StreamViewType.NEW_AND_OLD_IMAGES);

// Attach Lambda to stream
streamProcessor.addEventSource(
  new lambdaEventSources.DynamoEventSource(pricesTable, {
    startingPosition: lambda.StartingPosition.LATEST,
    batchSize: 100,  // Process up to 100 records per invocation
    bisectBatchOnError: true,
    retryAttempts: 3
  })
);

Code Structure and Patterns

Project Structure

pricing-service/
ā”œā”€ā”€ src/
│   ā”œā”€ā”€ resolvers/              # GraphQL resolvers (Node.js)
│   │   ā”œā”€ā”€ pricing.ts
│   │   ā”œā”€ā”€ store.ts
│   │   └── index.ts
│   ā”œā”€ā”€ lambda/                 # Lambda handlers (Node.js)
│   │   ā”œā”€ā”€ pricing-resolver/
│   │   │   └── index.ts        # AppSync resolver handler
│   │   ā”œā”€ā”€ stream-processor/
│   │   │   └── index.ts        # DynamoDB stream handler
│   │   └── scheduler/
│   │       └── index.ts        # Price change scheduler
│   ā”œā”€ā”€ services/               # Business logic (Node.js)
│   │   ā”œā”€ā”€ pricing-service.ts
│   │   ā”œā”€ā”€ dynamo-service.ts
│   │   └── notification-service.ts
│   ā”œā”€ā”€ utils/                  # Utilities (Node.js)
│   │   ā”œā”€ā”€ logger.ts
│   │   ā”œā”€ā”€ errors.ts
│   │   └── validation.ts
│   └── types/                  # TypeScript types
│       ā”œā”€ā”€ pricing.ts
│       └── graphql.ts
ā”œā”€ā”€ dist/                       # Compiled JavaScript (Node.js)
ā”œā”€ā”€ infrastructure/             # AWS CDK (TypeScript)
│   ā”œā”€ā”€ app.ts
│   ā”œā”€ā”€ pricing-stack.ts
│   └── lambda-stack.ts
ā”œā”€ā”€ package.json                # Node.js dependencies
ā”œā”€ā”€ tsconfig.json               # TypeScript config
└── schema.graphql              # GraphQL schema

Node.js Patterns Used

1. Async/Await Pattern

// Node.js async/await for non-blocking I/O
export const getPrice = async (storeId: string, productId: string) => {
  // Non-blocking DynamoDB query
  const result = await dynamoDB.get({
    TableName: 'prices',
    Key: { storeId, productId }
  });
  
  return result.Item;
};

2. Promise Chaining

// Node.js Promise chains for complex operations
export const updatePrice = async (input: UpdatePriceInput) => {
  return dynamoDB
    .put({ TableName: 'prices', Item: input })
    .then(() => dynamoDB.get({ TableName: 'prices', Key: { ... } }))
    .then(result => result.Item)
    .catch(error => {
      logger.error('Price update failed', error);
      throw error;
    });
};

3. Error Handling

// Node.js error handling patterns
export const handler = async (event: AppSyncResolverEvent) => {
  try {
    return await processRequest(event);
  } catch (error) {
    // Node.js error logging
    console.error('Handler error:', error);
    
    // Return GraphQL error format
    throw new Error(`Failed to process request: ${error.message}`);
  }
};

4. Module Exports (CommonJS)

// Node.js module exports
export const handler = async (event: any) => {
  // Handler logic
};

// Compiled to:
// exports.handler = async (event) => { ... };

5. Environment Variables

// Node.js process.env access
const PRICES_TABLE = process.env.PRICES_TABLE!;
const NODE_ENV = process.env.NODE_ENV || 'development';

// Used in Lambda environment configuration

DRF-Equivalent Patterns (Django REST Framework)

The service uses DRF-equivalent patterns in TypeScript/Node.js:

// Django-like serializer pattern in Node.js
class PriceSerializer {
  static validate(data: any): PriceInput {
    if (!data.storeId) throw new ValidationError('storeId required');
    if (!data.productId) throw new ValidationError('productId required');
    if (data.amount <= 0) throw new ValidationError('amount must be positive');
    return data;
  }
  
  static toRepresentation(price: Price): PriceOutput {
    return {
      storeId: price.storeId,
      productId: price.productId,
      amount: price.amount,
      currency: price.currency,
      effectiveDate: price.effectiveDate.toISOString()
    };
  }
}

// Django-like viewset pattern in Node.js
class PriceViewSet {
  async retrieve(storeId: string, productId: string): Promise<Price> {
    const price = await this.service.getPrice(storeId, productId);
    if (!price) throw new NotFoundError('Price not found');
    return PriceSerializer.toRepresentation(price);
  }
  
  async list(storeId: string, filters?: PriceFilter): Promise<Price[]> {
    const prices = await this.service.listPrices(storeId, filters);
    return prices.map(PriceSerializer.toRepresentation);
  }
  
  async create(input: CreatePriceInput): Promise<Price> {
    const validated = PriceSerializer.validate(input);
    const price = await this.service.createPrice(validated);
    return PriceSerializer.toRepresentation(price);
  }
}

Deployment and Execution

Build Process

# 1. Install Node.js dependencies
npm install

# 2. Compile TypeScript to JavaScript (for Node.js)
npm run build
# Runs: tsc (TypeScript Compiler)
# Output: dist/ (JavaScript files for Node.js)

# 3. Package for Lambda deployment
npm run package
# Creates: lambda-package.zip (Node.js code + dependencies)

# 4. Deploy with AWS CDK
cdk deploy
# Deploys Lambda functions with Node.js runtime

Package.json (Node.js Dependencies)

{
  "name": "pricing-service",
  "version": "1.0.0",
  "scripts": {
    "build": "tsc",
    "package": "zip -r lambda-package.zip dist node_modules",
    "deploy": "cdk deploy"
  },
  "dependencies": {
    "@aws-sdk/client-dynamodb": "^3.0.0",
    "@aws-sdk/lib-dynamodb": "^3.0.0",
    "@aws-sdk/client-appsync": "^3.0.0",
    "graphql": "^16.0.0"
  },
  "devDependencies": {
    "@types/node": "^18.0.0",
    "@types/aws-lambda": "^8.10.0",
    "typescript": "^5.0.0"
  },
  "engines": {
    "node": ">=18.0.0"
  }
}

AWS CDK Deployment

// infrastructure/pricing-stack.ts
import * as cdk from 'aws-cdk-lib';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class PricingServiceStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Deploy Lambda function with Node.js runtime
    const resolverFunction = new lambda.Function(this, 'PricingResolver', {
      runtime: lambda.Runtime.NODEJS_18_X,  // Node.js 18.x
      handler: 'index.handler',              // Node.js entry point
      code: lambda.Code.fromAsset('dist'),    // Compiled JavaScript
      timeout: cdk.Duration.seconds(30),
      memorySize: 512,
      environment: {
        NODE_ENV: 'production',
        PRICES_TABLE: pricesTable.tableName
      }
    });
  }
}

Execution Flow

1. Developer writes TypeScript code
   ↓
2. TypeScript compiles to JavaScript
   ↓
3. JavaScript packaged with Node.js dependencies
   ↓
4. AWS CDK deploys to Lambda
   ↓
5. Lambda uses Node.js 18.x runtime
   ↓
6. GraphQL request arrives at AppSync
   ↓
7. AppSync invokes Lambda (Node.js)
   ↓
8. Node.js executes handler function
   ↓
9. Handler queries DynamoDB (async)
   ↓
10. Result returned to AppSync
   ↓
11. Response sent to client

Performance Optimization

Node.js-Specific Optimizations

1. Connection Pooling

// Reuse DynamoDB client across Lambda invocations (warm start)
const dynamoClient = new DynamoDBClient({
  region: 'us-east-1',
  maxAttempts: 3
});

// Connection pool persists in warm Lambda containers
export const docClient = DynamoDBDocumentClient.from(dynamoClient);

2. Module Caching

// Modules loaded once and cached by Node.js
// Subsequent invocations reuse cached modules
import { dynamoClient } from './dynamo-client';  // Cached
import { logger } from './logger';                // Cached

3. Async Concurrency

// Node.js processes multiple async operations concurrently
export const batchUpdatePrices = async (updates: PriceUpdate[]) => {
  // All operations run concurrently (non-blocking)
  const results = await Promise.all(
    updates.map(update => updatePrice(update))
  );
  return results;
};

4. Memory Management

// Node.js garbage collection optimization
// - Use object pooling for frequently created objects
// - Avoid memory leaks with proper cleanup
// - Monitor memory usage in CloudWatch

export const handler = async (event: any) => {
  // Process request
  const result = await processRequest(event);
  
  // Explicit cleanup (helps Node.js GC)
  event = null;
  
  return result;
};

Single-Digit Millisecond Latency

Achieved through:

  1. Warm Lambda containers (Node.js already loaded)
  2. Connection pooling (reused DynamoDB connections)
  3. Efficient Node.js execution (V8 engine optimizations)
  4. Minimal dependencies (faster cold starts)
  5. Provisioned concurrency (always-warm containers)

Development Workflow

Local Development (Node.js)

# 1. Install Node.js (v18+)
node --version  # v18.x.x

# 2. Install dependencies
npm install

# 3. Run TypeScript compiler in watch mode
npm run dev
# Compiles TypeScript → JavaScript on file changes

# 4. Test locally with Node.js
npm run test
# Runs tests in Node.js environment

# 5. Run Lambda function locally (Node.js)
sam local invoke PricingResolver
# or
node dist/lambda/pricing-resolver/index.js

Testing Node.js Code

// tests/pricing.test.ts
import { handler } from '../src/lambda/pricing-resolver';
import type { AppSyncResolverEvent } from 'aws-lambda';

describe('Pricing Resolver', () => {
  it('should get price', async () => {
    const event: AppSyncResolverEvent = {
      arguments: { storeId: '123', productId: '456' },
      info: { fieldName: 'getPrice' }
    };
    
    // Test Node.js handler
    const result = await handler(event, {} as Context);
    
    expect(result).toBeDefined();
    expect(result.storeId).toBe('123');
  });
});

Debugging Node.js Lambda

// Enable Node.js debugging
export const handler = async (event: any) => {
  // Node.js console.log (appears in CloudWatch)
  console.log('Event:', JSON.stringify(event, null, 2));
  
  // Node.js error logging
  console.error('Error:', error);
  
  // Structured logging
  logger.info('Processing request', { storeId: event.arguments.storeId });
};

Summary

The pricing-service is built entirely on Node.js through:

  1. TypeScript → JavaScript compilation for Node.js runtime
  2. AWS Lambda with Node.js 18.x runtime
  3. GraphQL resolvers executing as Node.js functions
  4. Event-driven processing via Node.js Lambda handlers
  5. Async/await patterns leveraging Node.js non-blocking I/O
  6. CommonJS modules (Node.js module system)
  7. Performance optimizations specific to Node.js execution

The service demonstrates production-grade Node.js expertise at scale:

  • Millions of daily requests handled by Node.js Lambda functions
  • Single-digit millisecond latency through Node.js optimizations
  • 8,000+ stores served by Node.js-powered GraphQL API
  • Event-driven architecture built on Node.js async capabilities

This architecture showcases deep understanding of Node.js runtime characteristics, serverless execution models, and high-performance TypeScript/JavaScript development patterns.