Weekend Project: Develop a Secure, Optimized API with Advanced Architecture Patterns

Module 26: Advanced Backend & API Development

Introduction to the Weekend Project

In this weekend project, you'll apply the advanced backend and API development concepts we've covered throughout Module 26 to build a secure, optimized API with advanced architecture patterns. You'll develop a complete backend system for a digital marketplace application that incorporates microservices, serverless functions, and API gateway patterns.

To guide our development process, we'll use George Polya's 4-step problem-solving procedure, a proven methodology for tackling complex problems:

graph TD A["Polya's 4-Step Problem Solving"] --> B["1. Understand the Problem"] A --> C["2. Devise a Plan"] A --> D["3. Carry Out the Plan"] A --> E["4. Look Back and Reflect"] B --> B1["Define requirements"] B --> B2["Identify constraints"] B --> B3["Clarify expectations"] C --> C1["Design architecture"] C --> C2["Select technologies"] C --> C3["Plan implementation steps"] D --> D1["Implement components"] D --> D2["Test functionality"] D --> D3["Integrate systems"] E --> E1["Evaluate solution"] E --> E2["Identify improvements"] E --> E3["Document lessons learned"]

This structured approach will help us develop a robust, secure, and optimized API solution while ensuring we fully understand the problem before diving into implementation.

Step 1: Understand the Problem

Following Polya's first step, we need to thoroughly understand what we're building, why we're building it, and what constraints we face.

The Digital Marketplace Requirements

Our task is to build the backend API for a digital marketplace where users can buy and sell digital products (e.g., software, e-books, design templates). The system needs to:

Technical Requirements and Constraints

Understanding the Problem: Key Questions

Before proceeding, ask yourself these questions:

  • What is the core functionality that the system must provide?
  • Who are the different types of users and what are their needs?
  • What are the security concerns specific to a digital marketplace?
  • What are the performance bottlenecks we might encounter?
  • How might the system need to scale in the future?
  • What types of failures could occur, and how should we handle them?

Understanding Through Mapping User Journeys

Let's map out the primary user journeys to better understand the system's requirements:

sequenceDiagram participant B as Buyer participant S as Seller participant A as API participant D as Database participant P as Payment Processor S->>A: Register Account A->>D: Store User Info S->>A: Create Product Listing A->>D: Store Product Info B->>A: Register Account A->>D: Store User Info B->>A: Search for Products A->>D: Query Products D->>A: Return Results A->>B: Display Search Results B->>A: View Product Details A->>D: Get Product Info D->>A: Return Product Data A->>B: Show Product Details B->>A: Purchase Product A->>P: Process Payment P->>A: Confirm Payment A->>D: Update Order Status A->>B: Provide Download Link B->>A: Leave Review A->>D: Store Review S->>A: View Sales Analytics A->>D: Retrieve Sales Data D->>A: Return Analytics A->>S: Display Analytics Dashboard

By mapping these user journeys, we gain a clearer understanding of the functional requirements and data flows in our system.

Step 2: Devise a Plan

Now that we understand the problem, we can devise a plan for our solution. We'll design the overall architecture and select appropriate technologies and patterns.

Architectural Design

We'll use a combination of microservices and serverless architecture to build a scalable, maintainable system:

graph TD A[API Gateway] --> B[Auth Service] A --> C[Product Service] A --> D[Order Service] A --> E[Review Service] A --> F[Search Service] A --> G[Analytics Service] B -->|JWT Validation| A C --> H[(Product DB)] D --> I[(Order DB)] D --> J[Payment Processor] D --> K[Notification SQS] E --> L[(Review DB)] F --> M[ElasticSearch] G --> N[Data Warehouse] K --> O[Email Lambda] K --> P[Push Notification Lambda] style A fill:#f9d5e5,stroke:#333 style B,C,D,E,F,G fill:#d5e8d4,stroke:#333 style H,I,L,M,N fill:#dae8fc,stroke:#333 style J fill:#fff2cc,stroke:#333 style K fill:#ffe6cc,stroke:#333 style O,P fill:#e1d5e7,stroke:#333

Technology Selection

Component Technology Rationale
API Gateway AWS API Gateway Provides security, monitoring, rate limiting, and request/response transformation
Authentication Amazon Cognito + JWT Managed authentication service with scalability and security features
Core Services AWS Lambda + Express.js Serverless execution model for cost optimization and auto-scaling
Databases Amazon DynamoDB Fully managed NoSQL database with automatic scaling and low latency
Search Amazon Elasticsearch Service Powerful full-text search capabilities with high performance
Queuing Amazon SQS Reliable message queuing for decoupling services
Analytics Amazon Redshift Data warehousing for analytical queries
Deployment Serverless Framework Simplifies deployment and management of serverless applications

Implementation Plan

We'll break the implementation into manageable tasks:

  1. Core Infrastructure Setup: Configure API Gateway, Cognito, and base Lambda functions
  2. Authentication Service: Implement user registration, login, and JWT validation
  3. Product Service: Create CRUD operations for digital products
  4. Order Service: Develop the purchase flow and payment integration
  5. Review Service: Build the customer review functionality
  6. Search Service: Implement product search with Elasticsearch
  7. Analytics Service: Develop basic analytics capabilities
  8. Integration and Testing: Ensure all services work together seamlessly
  9. Security Hardening: Add additional security measures and conduct testing
  10. Performance Optimization: Identify and resolve performance bottlenecks

Advanced Patterns We'll Implement

  • Circuit Breaker Pattern: Prevent cascade failures when a service is down
  • CQRS (Command Query Responsibility Segregation): Separate read and write operations
  • Event Sourcing: Store changes to the application state as a sequence of events
  • API Gateway Pattern: Central entry point for all client requests
  • Backend for Frontend (BFF): API gateway layer customized for specific clients
  • Saga Pattern: Manage distributed transactions across services

Step 3: Carry Out the Plan

Now that we have a plan, let's implement the solution. We'll focus on key components and patterns in this section.

Setting Up the Project Structure


# Create project directory
mkdir digital-marketplace-api
cd digital-marketplace-api

# Initialize with Serverless Framework
serverless create --template aws-nodejs

# Create service directories
mkdir -p services/{auth,products,orders,reviews,search,analytics}

# Initialize package.json
npm init -y

# Install common dependencies
npm install --save aws-sdk jsonwebtoken uuid joi
npm install --save-dev serverless-offline serverless-iam-roles-per-function
      

Main serverless.yml Configuration


# serverless.yml
service: digital-marketplace-api

frameworkVersion: '3'

provider:
  name: aws
  runtime: nodejs16.x
  stage: ${opt:stage, 'dev'}
  region: ${opt:region, 'us-east-1'}
  environment:
    STAGE: ${self:provider.stage}
    REGION: ${self:provider.region}
  apiGateway:
    minimumCompressionSize: 1024
    shouldStartNameWithService: true
  logs:
    restApi: true

plugins:
  - serverless-offline
  - serverless-iam-roles-per-function

package:
  individually: true
  excludeDevDependencies: true

custom:
  tableName: ${self:service}-${self:provider.stage}
  userPoolName: ${self:service}-user-pool-${self:provider.stage}
  serverless-offline:
    httpPort: 3000
    noPrependStageInUrl: true
  
resources:
  Resources:
    # Cognito User Pool
    CognitoUserPool:
      Type: AWS::Cognito::UserPool
      Properties:
        UserPoolName: ${self:custom.userPoolName}
        AutoVerifiedAttributes:
          - email
        Policies:
          PasswordPolicy:
            MinimumLength: 8
            RequireLowercase: true
            RequireNumbers: true
            RequireSymbols: false
            RequireUppercase: true
        Schema:
          - Name: email
            AttributeDataType: String
            Mutable: false
            Required: true
          - Name: name
            AttributeDataType: String
            Mutable: true
            Required: true
    
    # Cognito User Pool Client
    CognitoUserPoolClient:
      Type: AWS::Cognito::UserPoolClient
      Properties:
        ClientName: ${self:service}-client-${self:provider.stage}
        UserPoolId:
          Ref: CognitoUserPool
        ExplicitAuthFlows:
          - ALLOW_USER_SRP_AUTH
          - ALLOW_REFRESH_TOKEN_AUTH
        PreventUserExistenceErrors: ENABLED
        GenerateSecret: false
    
    # DynamoDB Tables
    ProductsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}-products
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: sellerId
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: SellerIdIndex
            KeySchema:
              - AttributeName: sellerId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
    
    OrdersTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}-orders
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: buyerId
            AttributeType: S
          - AttributeName: sellerId
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: BuyerIdIndex
            KeySchema:
              - AttributeName: buyerId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
          - IndexName: SellerIdIndex
            KeySchema:
              - AttributeName: sellerId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
    
    ReviewsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.tableName}-reviews
        BillingMode: PAY_PER_REQUEST
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
          - AttributeName: productId
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        GlobalSecondaryIndexes:
          - IndexName: ProductIdIndex
            KeySchema:
              - AttributeName: productId
                KeyType: HASH
            Projection:
              ProjectionType: ALL
      

Authentication Service Implementation


// services/auth/register.js
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const Joi = require('joi');

const cognito = new AWS.CognitoIdentityServiceProvider();
const dynamoDB = new AWS.DynamoDB.DocumentClient();

// Validation schema
const schema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
  name: Joi.string().required(),
  role: Joi.string().valid('buyer', 'seller').required()
});

module.exports.handler = async (event) => {
  try {
    // Parse and validate input
    const body = JSON.parse(event.body);
    const validation = schema.validate(body);
    
    if (validation.error) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          error: 'Validation Error',
          details: validation.error.details
        })
      };
    }
    
    const { email, password, name, role } = body;
    
    // Register user in Cognito
    const userPoolId = process.env.USER_POOL_ID;
    
    const params = {
      UserPoolId: userPoolId,
      Username: email,
      TemporaryPassword: password,
      UserAttributes: [
        {
          Name: 'email',
          Value: email
        },
        {
          Name: 'name',
          Value: name
        },
        {
          Name: 'custom:role',
          Value: role
        }
      ]
    };
    
    const cognitoResult = await cognito.adminCreateUser(params).promise();
    const userId = cognitoResult.User.Username;
    
    // Set permanent password
    await cognito.adminSetUserPassword({
      UserPoolId: userPoolId,
      Username: userId,
      Password: password,
      Permanent: true
    }).promise();
    
    // Store additional user data in DynamoDB
    const user = {
      id: userId,
      email,
      name,
      role,
      createdAt: new Date().toISOString()
    };
    
    await dynamoDB.put({
      TableName: process.env.USERS_TABLE,
      Item: user
    }).promise();
    
    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        message: 'User registered successfully',
        userId
      })
    };
  } catch (error) {
    console.error('Error registering user:', error);
    
    // Handle specific errors
    if (error.code === 'UsernameExistsException') {
      return {
        statusCode: 409,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          error: 'User already exists'
        })
      };
    }
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message
      })
    };
  }
};
      

Product Service with CQRS Pattern


// services/products/create.js - Command part of CQRS
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const Joi = require('joi');

const dynamoDB = new AWS.DynamoDB.DocumentClient();
const eventBridge = new AWS.EventBridge();

// Validation schema
const schema = Joi.object({
  title: Joi.string().required(),
  description: Joi.string().required(),
  price: Joi.number().positive().required(),
  category: Joi.string().required(),
  files: Joi.array().items(Joi.object({
    name: Joi.string().required(),
    type: Joi.string().required(),
    size: Joi.number().required(),
    url: Joi.string().uri().required()
  })).min(1).required()
});

module.exports.handler = async (event) => {
  try {
    // Verify authentication
    const userId = event.requestContext.authorizer.claims.sub;
    
    // Parse and validate input
    const body = JSON.parse(event.body);
    const validation = schema.validate(body);
    
    if (validation.error) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          error: 'Validation Error',
          details: validation.error.details
        })
      };
    }
    
    // Create product
    const { title, description, price, category, files } = body;
    
    const product = {
      id: uuidv4(),
      sellerId: userId,
      title,
      description,
      price,
      category,
      files,
      status: 'active',
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString()
    };
    
    // Save to DynamoDB
    await dynamoDB.put({
      TableName: process.env.PRODUCTS_TABLE,
      Item: product
    }).promise();
    
    // Publish event for search indexing and analytics
    await eventBridge.putEvents({
      Entries: [{
        Source: 'digital-marketplace.products',
        DetailType: 'ProductCreated',
        Detail: JSON.stringify(product),
        EventBusName: 'default'
      }]
    }).promise();
    
    return {
      statusCode: 201,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(product)
    };
  } catch (error) {
    console.error('Error creating product:', error);
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message
      })
    };
  }
};
      

// services/products/search.js - Query part of CQRS
const AWS = require('aws-sdk');

// Use Elasticsearch for efficient queries
const elasticsearch = require('elasticsearch');

// Circuit breaker pattern implementation
const circuitBreaker = require('../../lib/circuitBreaker');

// Initialize Elasticsearch client with circuit breaker
const esClient = circuitBreaker.wrap(
  new elasticsearch.Client({
    host: process.env.ELASTICSEARCH_ENDPOINT,
    log: 'error'
  })
);

module.exports.handler = async (event) => {
  try {
    // Extract query parameters
    const query = event.queryStringParameters || {};
    const searchTerm = query.q || '';
    const category = query.category;
    const minPrice = query.minPrice ? parseFloat(query.minPrice) : undefined;
    const maxPrice = query.maxPrice ? parseFloat(query.maxPrice) : undefined;
    const page = parseInt(query.page || '1', 10);
    const size = parseInt(query.size || '20', 10);
    
    // Build Elasticsearch query
    const esQuery = {
      bool: {
        must: searchTerm ? {
          multi_match: {
            query: searchTerm,
            fields: ['title^3', 'description', 'category']
          }
        } : { match_all: {} },
        filter: []
      }
    };
    
    // Add filters
    if (category) {
      esQuery.bool.filter.push({
        term: { category }
      });
    }
    
    if (minPrice !== undefined || maxPrice !== undefined) {
      const range = {};
      if (minPrice !== undefined) range.gte = minPrice;
      if (maxPrice !== undefined) range.lte = maxPrice;
      
      esQuery.bool.filter.push({
        range: { price: range }
      });
    }
    
    // Only show active products
    esQuery.bool.filter.push({
      term: { status: 'active' }
    });
    
    // Execute search query
    const result = await esClient.search({
      index: process.env.PRODUCTS_INDEX,
      body: {
        query: esQuery,
        sort: [
          { _score: { order: 'desc' } },
          { createdAt: { order: 'desc' } }
        ],
        from: (page - 1) * size,
        size
      }
    });
    
    // Format response
    const products = result.hits.hits.map(hit => ({
      id: hit._id,
      ...hit._source,
      score: hit._score
    }));
    
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Cache-Control': 'max-age=60'  // Cache for 60 seconds
      },
      body: JSON.stringify({
        total: result.hits.total.value,
        page,
        size,
        products
      })
    };
  } catch (error) {
    console.error('Error searching products:', error);
    
    // If circuit is open, return cached results
    if (error.name === 'CircuitBreakerError') {
      return {
        statusCode: 200,
        headers: {
          'Content-Type': 'application/json',
          'X-Data-Source': 'fallback'
        },
        body: JSON.stringify({
          message: 'Using fallback search results',
          products: await getFallbackProducts(event.queryStringParameters)
        })
      };
    }
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message
      })
    };
  }
};

// Fallback function for when Elasticsearch is down
async function getFallbackProducts(query) {
  const dynamoDB = new AWS.DynamoDB.DocumentClient();
  
  // Simple scan with basic filtering
  const params = {
    TableName: process.env.PRODUCTS_TABLE,
    Limit: 20,
    FilterExpression: 'status = :status',
    ExpressionAttributeValues: {
      ':status': 'active'
    }
  };
  
  // Add category filter if provided
  if (query && query.category) {
    params.FilterExpression += ' AND category = :category';
    params.ExpressionAttributeValues[':category'] = query.category;
  }
  
  const result = await dynamoDB.scan(params).promise();
  return result.Items;
}
      

Order Service with Saga Pattern


// services/orders/createOrder.js - Saga coordinator
const AWS = require('aws-sdk');
const { v4: uuidv4 } = require('uuid');
const Joi = require('joi');

const dynamoDB = new AWS.DynamoDB.DocumentClient();
const stepFunctions = new AWS.StepFunctions();

// Validation schema
const schema = Joi.object({
  productId: Joi.string().required(),
  paymentMethodId: Joi.string().required()
});

module.exports.handler = async (event) => {
  try {
    // Verify authentication
    const buyerId = event.requestContext.authorizer.claims.sub;
    
    // Parse and validate input
    const body = JSON.parse(event.body);
    const validation = schema.validate(body);
    
    if (validation.error) {
      return {
        statusCode: 400,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          error: 'Validation Error',
          details: validation.error.details
        })
      };
    }
    
    const { productId, paymentMethodId } = body;
    
    // Get product details
    const productResult = await dynamoDB.get({
      TableName: process.env.PRODUCTS_TABLE,
      Key: { id: productId }
    }).promise();
    
    const product = productResult.Item;
    
    if (!product) {
      return {
        statusCode: 404,
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          error: 'Product not found'
        })
      };
    }
    
    // Create order
    const orderId = uuidv4();
    const now = new Date().toISOString();
    
    const order = {
      id: orderId,
      buyerId,
      sellerId: product.sellerId,
      productId,
      productTitle: product.title,
      price: product.price,
      status: 'pending',
      paymentStatus: 'pending',
      deliveryStatus: 'pending',
      paymentMethodId,
      createdAt: now,
      updatedAt: now
    };
    
    // Save initial order
    await dynamoDB.put({
      TableName: process.env.ORDERS_TABLE,
      Item: order
    }).promise();
    
    // Start Saga (Step Functions state machine)
    const sagaInput = {
      orderId,
      buyerId,
      sellerId: product.sellerId,
      productId,
      amount: product.price,
      paymentMethodId
    };
    
    await stepFunctions.startExecution({
      stateMachineArn: process.env.ORDER_SAGA_STATE_MACHINE,
      input: JSON.stringify(sagaInput),
      name: `Order-${orderId}-${Date.now()}`
    }).promise();
    
    return {
      statusCode: 202,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        message: 'Order created and processing started',
        orderId,
        status: 'pending'
      })
    };
  } catch (error) {
    console.error('Error creating order:', error);
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message
      })
    };
  }
};
      

Order Saga State Machine Definition


// serverless.yml fragment for Order Saga state machine
resources:
  Resources:
    OrderSagaStateMachine:
      Type: AWS::StepFunctions::StateMachine
      Properties:
        StateMachineName: ${self:service}-order-saga-${self:provider.stage}
        RoleArn: !GetAtt OrderSagaExecutionRole.Arn
        DefinitionString: !Sub |
          {
            "Comment": "Order Processing Saga",
            "StartAt": "ProcessPayment",
            "States": {
              "ProcessPayment": {
                "Type": "Task",
                "Resource": "${ProcessPaymentFunction.Arn}",
                "Next": "UpdateInventory",
                "Catch": [
                  {
                    "ErrorEquals": ["PaymentFailedError"],
                    "Next": "FailOrder"
                  }
                ]
              },
              "UpdateInventory": {
                "Type": "Task",
                "Resource": "${UpdateInventoryFunction.Arn}",
                "Next": "PrepareDelivery",
                "Catch": [
                  {
                    "ErrorEquals": ["InventoryError"],
                    "Next": "RefundPayment"
                  }
                ]
              },
              "PrepareDelivery": {
                "Type": "Task",
                "Resource": "${PrepareDeliveryFunction.Arn}",
                "Next": "NotifyBuyer",
                "Catch": [
                  {
                    "ErrorEquals": ["DeliveryError"],
                    "Next": "RollbackInventory"
                  }
                ]
              },
              "NotifyBuyer": {
                "Type": "Task",
                "Resource": "${NotifyBuyerFunction.Arn}",
                "Next": "CompleteOrder",
                "Catch": [
                  {
                    "ErrorEquals": ["NotificationError"],
                    "Next": "CompleteOrder"
                  }
                ]
              },
              "CompleteOrder": {
                "Type": "Task",
                "Resource": "${CompleteOrderFunction.Arn}",
                "End": true
              },
              "RefundPayment": {
                "Type": "Task",
                "Resource": "${RefundPaymentFunction.Arn}",
                "Next": "FailOrder",
                "Catch": [
                  {
                    "ErrorEquals": ["RefundError"],
                    "Next": "FailOrderWithAlert"
                  }
                ]
              },
              "RollbackInventory": {
                "Type": "Task",
                "Resource": "${RollbackInventoryFunction.Arn}",
                "Next": "RefundPayment"
              },
              "FailOrder": {
                "Type": "Task",
                "Resource": "${FailOrderFunction.Arn}",
                "End": true
              },
              "FailOrderWithAlert": {
                "Type": "Task",
                "Resource": "${FailOrderWithAlertFunction.Arn}",
                "End": true
              }
            }
          }
      

API Gateway Implementation with Backend for Frontend (BFF) Pattern


// services/bff/mobileApi.js - Mobile BFF example
const AWS = require('aws-sdk');
const lambda = new AWS.Lambda();

module.exports.handler = async (event) => {
  try {
    const route = event.path;
    const method = event.httpMethod;
    const userId = event.requestContext.authorizer?.claims?.sub;
    
    // Mobile-specific data aggregation for product details
    if (route.startsWith('/products/') && method === 'GET') {
      const productId = route.split('/')[2];
      
      // Parallel requests to gather all needed data
      const [productResult, reviewsResult, relatedResult] = await Promise.all([
        // Get product details
        lambda.invoke({
          FunctionName: process.env.GET_PRODUCT_FUNCTION,
          Payload: JSON.stringify({ pathParameters: { id: productId } })
        }).promise(),
        
        // Get product reviews
        lambda.invoke({
          FunctionName: process.env.LIST_REVIEWS_FUNCTION,
          Payload: JSON.stringify({ 
            queryStringParameters: { 
              productId,
              limit: 3  // Mobile shows fewer reviews
            } 
          })
        }).promise(),
        
        // Get related products
        lambda.invoke({
          FunctionName: process.env.GET_RELATED_PRODUCTS_FUNCTION,
          Payload: JSON.stringify({ pathParameters: { id: productId } })
        }).promise()
      ]);
      
      // Parse results
      const product = JSON.parse(productResult.Payload);
      const reviews = JSON.parse(reviewsResult.Payload);
      const relatedProducts = JSON.parse(relatedResult.Payload);
      
      // Check if user has purchased this product
      let hasProductAccess = false;
      if (userId) {
        const accessResult = await lambda.invoke({
          FunctionName: process.env.CHECK_PRODUCT_ACCESS_FUNCTION,
          Payload: JSON.stringify({ 
            userId,
            productId
          })
        }).promise();
        
        hasProductAccess = JSON.parse(accessResult.Payload).hasAccess;
      }
      
      // Optimize response for mobile
      if (product.statusCode === 200) {
        const productData = JSON.parse(product.body);
        
        // Remove large descriptions for mobile preview
        if (productData.description && productData.description.length > 200) {
          productData.shortDescription = productData.description.substring(0, 200) + '...';
          delete productData.description;
        }
        
        // Optimize images for mobile
        if (productData.files) {
          productData.files = productData.files.map(file => {
            if (file.type.startsWith('image/')) {
              // Replace high-res URLs with mobile-optimized ones
              file.mobileUrl = file.url.replace('/original/', '/mobile/');
            }
            return file;
          });
        }
        
        // Add aggregated data
        productData.reviews = JSON.parse(reviews.body).reviews || [];
        productData.relatedProducts = JSON.parse(relatedProducts.body).products || [];
        productData.hasAccess = hasProductAccess;
        
        return {
          statusCode: 200,
          headers: {
            'Content-Type': 'application/json',
            'Cache-Control': 'max-age=300'  // Cache for 5 minutes
          },
          body: JSON.stringify(productData)
        };
      }
      
      // Pass through the error from product service
      return product;
    }
    
    // Handle other routes
    // ...
    
    // Default: not found
    return {
      statusCode: 404,
      body: JSON.stringify({ error: 'Not Found' })
    };
  } catch (error) {
    console.error('Error in Mobile BFF:', error);
    
    return {
      statusCode: 500,
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        error: 'Internal Server Error',
        message: error.message
      })
    };
  }
};
      

Circuit Breaker Implementation


// lib/circuitBreaker.js
class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.resetTimeout = options.resetTimeout || 30000; // 30 seconds
    this.failureCount = 0;
    this.state = 'CLOSED';
    this.nextAttempt = Date.now();
    this.fallback = options.fallback;
    this.name = options.name || 'circuit';
    
    // For logging/monitoring
    this.stats = {
      successful: 0,
      failed: 0,
      rejected: 0,
      lastError: null,
      lastErrorTime: null
    };
  }
  
  async exec(fn, ...args) {
    if (this.state === 'OPEN') {
      // Circuit is open, check if we should try again
      if (Date.now() > this.nextAttempt) {
        // Try again (half-open state)
        this.state = 'HALF-OPEN';
        console.log(`Circuit ${this.name} entering HALF-OPEN state`);
      } else {
        // Still open, reject the request
        this.stats.rejected++;
        console.log(`Circuit ${this.name} is OPEN, rejecting request`);
        
        if (this.fallback) {
          return this.fallback(...args);
        }
        
        const error = new Error(`Circuit ${this.name} is open`);
        error.name = 'CircuitBreakerError';
        throw error;
      }
    }
    
    try {
      // Execute the function
      const result = await fn(...args);
      
      // Successful execution
      this.onSuccess();
      return result;
    } catch (error) {
      // Failed execution
      this.onFailure(error);
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    this.stats.successful++;
    
    if (this.state === 'HALF-OPEN') {
      // Reset the circuit on successful half-open call
      this.state = 'CLOSED';
      console.log(`Circuit ${this.name} reset to CLOSED state`);
    }
  }
  
  onFailure(error) {
    this.stats.failed++;
    this.stats.lastError = error.message;
    this.stats.lastErrorTime = new Date().toISOString();
    
    if (this.state === 'HALF-OPEN') {
      // Failed during half-open state, reopen the circuit
      this.state = 'OPEN';
      this.nextAttempt = Date.now() + this.resetTimeout;
      console.log(`Circuit ${this.name} switched back to OPEN state`);
    } else if (this.state === 'CLOSED') {
      this.failureCount++;
      
      if (this.failureCount >= this.failureThreshold) {
        // Too many failures, open the circuit
        this.state = 'OPEN';
        this.nextAttempt = Date.now() + this.resetTimeout;
        console.log(`Circuit ${this.name} switched to OPEN state after ${this.failureCount} failures`);
      }
    }
  }
  
  // Wrap a service or function with the circuit breaker
  wrap(service) {
    // If service is an object with methods
    if (service && typeof service === 'object') {
      const wrappedService = {};
      
      // Wrap each method
      for (const prop in service) {
        if (typeof service[prop] === 'function') {
          const originalMethod = service[prop];
          
          wrappedService[prop] = async (...args) => {
            return this.exec(originalMethod.bind(service), ...args);
          };
        } else {
          wrappedService[prop] = service[prop];
        }
      }
      
      return wrappedService;
    }
    
    // If service is a function
    if (typeof service === 'function') {
      return async (...args) => {
        return this.exec(service, ...args);
      };
    }
    
    return service;
  }
  
  // Get current status
  getStats() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      nextAttempt: this.nextAttempt,
      stats: this.stats
    };
  }
}

// Export a factory function to create circuit breakers
module.exports = {
  create: (options) => new CircuitBreaker(options),
  wrap: (service, options = {}) => {
    const circuitBreaker = new CircuitBreaker(options);
    return circuitBreaker.wrap(service);
  }
};
      

Step 4: Look Back and Reflect

Following Polya's final step, let's reflect on our solution, evaluate its strengths and weaknesses, and identify areas for improvement.

Evaluating the Architecture

Our digital marketplace API implementation demonstrates several advanced architecture patterns:

Security Assessment

Our implementation includes several security measures:

Areas for further security improvement:

Performance Optimization

Performance considerations in our implementation:

Areas for further performance improvement:

Cost Optimization

Cost considerations in our implementation:

Areas for further cost optimization:

Lessons Learned

Implementing this project provides several valuable lessons:

Architecture Tradeoffs

Best Practices

Future Improvements

If we were to extend this project, we might consider:

Practical Exercise

Building a Specific Component

For this weekend project exercise, focus on implementing one of the key components of the digital marketplace API:

  1. Product Service: Implement the complete CRUD operations for products, including search functionality.
  2. Order Processing Saga: Implement the saga pattern for order processing with rollback mechanisms.
  3. Mobile BFF: Create a Backend for Frontend optimized for mobile clients.

Choose one of these components based on your interests and follow these steps:

  1. Set up a new Serverless Framework project for your chosen component
  2. Define the serverless.yml file with the necessary resources
  3. Implement the Lambda functions for your component
  4. Add appropriate error handling and security measures
  5. Write unit tests for your implementation
  6. Deploy the component to AWS
  7. Test your implementation using Postman or similar tools

For an extra challenge, implement monitoring using AWS CloudWatch and create a dashboard to visualize key metrics.

Conclusion

In this weekend project, we've designed and implemented a secure, optimized API for a digital marketplace using advanced architecture patterns. By following George Polya's 4-step problem-solving approach, we've:

  1. Understood the problem by analyzing the requirements and constraints of the digital marketplace
  2. Devised a plan by designing a microservices architecture with serverless components
  3. Carried out the plan by implementing key services with advanced patterns like CQRS, Saga, and Circuit Breaker
  4. Looked back and reflected on our solution to identify strengths, weaknesses, and future improvements

This approach has helped us create a robust, scalable, and maintainable API architecture that addresses the core requirements while also considering security, performance, and cost optimization.

The patterns and techniques demonstrated in this project are applicable to a wide range of applications beyond digital marketplaces. Understanding these advanced concepts will help you design better systems regardless of the specific domain.

Additional Resources