The Full Stack

Summary

This post is inspired by the article Developing Evolutionary Architecture with AWS Lambda. Instead of AWS Lambda, this post describes how to structure code for GCP Cloud Functions in a modular hexagonal architecture.

Mapping AWS components to GCP components we will use:

  • Google Cloud Function is the GCP serverless equivalent of AWS Lambda
  • GCP Datastore is the fully managed schema-less NoSQL database service instead of AWS DynamoDB
  • GCP Api Gateway in place of the AWS Api Gateway.

See source code

Hexagonal architecture pattern

In order to achieve evolution architecture we will use the hexagonal architecture approach to layout the code. Hexagonal architecture is an architectural pattern used for encapsulating domain logic and decoupling it from other implementation details, such as infrastructure or client requests. It is comprised of Ports, Adapters and Domain.

  1. Adapters: A design pattern for transforming one interface into another interface. Wraps the logic of interacting with the world outside our Cloud Function. In this case the DB datastore and the currency service api.
  2. Ports: The Adapters access the Domain logic via Ports. Domain logic also uses Ports to interact with the system outside via the Adapters.
  3. Domain logic: Represents the task that the application should perform. Which the business logic resides. External systems access via Ports.

Hexagonal architecture with Cloud Functions

Example The Cloud Function creates a HTTP endpoint that will accept a stock such as:

http://yourcloudfunction/?stockid=AAPL

and return the stock with the value in multiple currencies.

 {
  "stock": "AAPL",
  "values": {
    "EUR": "4.00",
    "USD": "4.73",
    "CAD": "5.95",
    "AUD": "6.41"
   }
}

The stock price is stored in DataStore and the currencies & rates are retrieved via a 3rd part currency service.

Code samples

Cloud Function Handler: index.js

"use strict";
const getStocksRequest =require("./adapters/GetStocksRequest");

exports.stocksGET = async (req, res) => {
  try {
    const responseData = await getStocksRequest(req.query.stockid);

    res.send(responseData);
  }
  catch (err) {
    console.error(err);
    res.send(err);
  }
};

Adapter: adapters/GetStocksRequest.js

const HTTPHandler = require("../ports/HTTPHandler");

const getStocksRequest = async (stockID) => {

  let responseData = {};

  try {
    responseData = await HTTPHandler.retrieveStock(stockID);
  } catch (err) {
   console.log(err);
   return err;
  }
  return responseData;
};

module.exports = getStocksRequest;

Port: ports/HTTPHandler.js

const stock = require("../domains/StocksLogic");

const retrieveStock = async (stockID) => {

  try {
    const stockWithCurrencies = await stock.retrieveStockValues(stockID);

    return stockWithCurrencies;
  } catch (err) {
    return err;
  }
};

module.exports = { retrieveStock, };

Domain: domains/StocksLogic.js

const CurrencyService = require("../ports/CurrencyService");
const StocksRepository = require("../ports/StocksRepository");
const CURRENCIES = ["USD", "CAD", "AUD"];

const retrieveStockValues = async (stockID) => {
  try {
    const stockValue = await StocksRepository.getStockData(stockID);

    const currencyList = await CurrencyService.getCurrenciesData(CURRENCIES);

    const stockWithCurrencies = {
      stock: stockValue.stockID,
      values: {
        EUR: stockValue.Value.toFixed(2),
      },
    };

    for (const currency in currencyList) {

    stockWithCurrencies.values[currency] = ( stockValue.Value * currencyList[currency] ).toFixed(2);
    }

    return stockWithCurrencies;
  } catch (err) {
    return err;
  }
};

module.exports = { retrieveStockValues, };

Tests Unit testing for an Adapter: tests/ports/StocksRepository.js

const getStockValue=require("../../adapters/StocksDAO");

const { getStockData } = require("../../ports/StocksRepository");

jest.mock("../../adapters/StocksDAO");

test("StockResposity returns data", async () => {

  const testStock = { stockID: "AAPL", Value: 4, };

  getStockValue.mockResolvedValue(testStock);

  getStockData().then((data) => {
    expect(data).toEqual(testStock);
  });
});

Benefits When I first created a Cloud Function I was uncertain of a suitable approach to structure the code. The hexagonal ports and adapters approach provided me with an opinionated loosely coupled design that helped structure the code, easier to make changes and helped with unit testing.

Developing Evolutionary Architecture with GCP Cloud Function

NodeJS
Serverless
Architecture
GCP

Be the first to share a comment.