マイクロアーキテクチャーに対して様々な異なる意見を聞くことが多いので、自分なりにちゃんとまとめてみました。

ソースコードを交え解説しようと思います。

マイクローアーキテクチャーで意識すること

  • 1つのLambda関数=1つの目的
  • Lambda関数同士は完全に疎結合
  • 極力共通化しない

API GatewayのLambdaProxy関数はリソース×メソッド単位で構築

例えば、ミドルウェア的な処理実行するために、1つのLambda関数を呼び出しそこでルーティング処理するなどは、ご法度です。
(Lambdaがそのミドルウェアに強く依存してしまう危険性があり、疎結合にならなくなってしまう可能性が高い)

ソースコードで言うと、下記のような粒度で構築するのがベストだと言えます。

マイクロアーキテクチャーの思想に則ったLambda関数

serverless.yaml

functions:
  getMe:
    handler: src/functions/me/get.handler
    events:
      - http:
          path: /me
          method: get
          authorizer: authorizer
  putMe:
    handler: src/functions/me/put.handler
    events:
      - http:
          path: /me
          method: put
          authorizer: authorizer

src/functions/me/get.handler
目的:ログイン中のユーザー情報を取得

import _ from 'lodash'
import {PREFIX, documentClient} from '../../awsUtils/dynamodb'
import {ERROR_HEADERS, SUCCESS_HEADERS} from "../../models/headerModel";

export async function handler(event, context, callback) {
  const userId = event.requestContext.authorizer.principalId;
  if (!userId || userId === 'NO_AUTH') return callback(null, {
    statusCode: 400,
    body: JSON.stringify({
      result: 'Invalid user'
    }),
    headers: ERROR_HEADERS
  });

  await documentClient
    .get({
      TableName: `${PREFIX}-users`,
      Key: {id: userId},
    })
    .promise()
    .then((res) => {
      if (res.Item) return callback(null, {
        statusCode: _.isEmpty(res.Item) ? 204 : 200,
        body: JSON.stringify({
          ...res.Item
        }),
        headers: SUCCESS_HEADERS
      });
      callback(null, {
        statusCode: 400,
        body: JSON.stringify({
          result: 'Bad parameter'
        }),
        headers: ERROR_HEADERS
      });
    })
    .catch(() => callback(null, {
      statusCode: 400,
      body: JSON.stringify({
        result: 'Bad parameter'
      }),
      headers: ERROR_HEADERS
    }))
}

src/functions/me/put.handler
目的:ログイン中のユーザー情報を更新

import _ from 'lodash'
import {PREFIX, documentClient} from '../../awsUtils/dynamodb'
import {ERROR_HEADERS, SUCCESS_HEADERS} from "../../models/headerModel";

export async function handler(event, context, callback) {
  const userId = event.requestContext.authorizer.principalId;
  if (!userId || userId === 'NO_AUTH') return callback(null, {
    statusCode: 400,
    body: JSON.stringify({
      result: 'Invalid user'
    }),
    headers: ERROR_HEADERS
  });

  const body = JSON.parse(event.body);

  await documentClient
    .get({
      TableName: `${PREFIX}-users`,
      Key: {id: userId},
    })
    .promise()
    .then((res) => res.Item)
    .then((me) => {
      const newItem = _.merge({}, me, _.pick(body, [
        'fullname',
        'company',
        'websiteUrl',
      ]), {id: userId})
      return documentClient.put({
        TableName: `${PREFIX}-users`,
        Item: newItem,
      })
        .promise()
        .then(() => callback(null, {
          statusCode: _.isEmpty(newItem) ? 204 : 200,
          body: JSON.stringify({
            ...newItem
          }),
          headers: SUCCESS_HEADERS
        }))
        .catch(() => callback(null, {
          statusCode: 400,
          body: JSON.stringify({
            result: 'Bad parameter'
          }),
          headers: ERROR_HEADERS
        }))
    })
}

Lambda中のソースコードは非常にシンプルで、初見でサクッと保守しやすいのがわかるかと思います。

PHPやJavaのフレームワークは便利機能を色々と提供してくれますが、継承などのOOP志向な仕組みにより、それなりに広範囲にソースコードを理解する必要があるので、キャッチアップや読解に時間を要します。

マイクロアーキテクチャーのLambda関数は、誰にでもわかりやすい人に依存しない簡潔なコードを提供してくれます。

非推奨のLambda関数の実装の仕方

serverless.yaml

functions:
  getMe:
    handler: src/functions/me/index.get
    ...
  putMe:
    handler: src/functions/me/index.put
    ...

src/functions/me/index.js

export async function get(event, context, callback) {
  ...
}
export async function put(event, context, callback) {
  ...
}

1つのファイルに目的が2つ以上存在し、見通しが悪くなってしまっています...。
テストもしにくくなってしまっています。

共通化は極力しないことを推奨

例えば、前述のソースコードだとこの辺りを共通化したくなるはずです。

  const userId = event.requestContext.authorizer.principalId;
  if (!userId || userId === 'NO_AUTH') return callback(null, {
    statusCode: 400,
    body: JSON.stringify({
      result: 'Invalid user'
    }),
    headers: ERROR_HEADERS
  });

でもあえて共通化しません。共通化のコードが増えれば増えるほど複雑になります。
この辺りはJava出身のエンジニアだとかなり違和感を感じると思いますが、共通化や継承化はプログラミングにそれなりに複雑さを与え、修正しにくくなっていきますし、Lambdaを疎結合からどんどん遠くさせます。

可能な限りLambda関数内で処理が完結するようにしましょう。

こんな時は共通化を検討したい

  • 外部の処理を扱うための手続き(DynamoDBへアクセスするための設定等)
  • レスポンスに固定値を与えるとき

まとめ

もちろん宗教論あると思いますが、私は実務上で最も開発がスムーズに進んだのは本記事のようなアーキテクチャーでした。
プルリクエストが非常に読みやすかったですし、フロントエンドの人や第三者の人が気軽に参加できるサーバーサイドを構築を行うことができました。