SaFi Bank Space : SaFi Code Architecture Guidelines (WIP)

Code Structure

Simple Services

Simple services refer to light-weight services that do not have much business logic in them. These could be CRUD utility services (e.g. merchants-manager which just tracks the merchants for each transaction type), or gateway services which act as a service layer for external APIs/dependencies.

Overview

A simple package structure should suffice for simple services, since a more complicated one just increases overhead and complexity unnecessarily.

Structure Description

ph.safibank.<projectname>.

  • controller - for controllers serving REST API

  • model - containing model in form of Kotlin data classes.

  • repository - containing repository classes (interfaces in case of using Micronaut Data Repository interfaces)

  • service - services layer. This layer is written usually in form of couples of interfaces & implementation. It is convention introduced by Spring authors and de-facto standard in java world.

  • utils - containing for example function for easier logger instantiation or set of exceptions specific to microservice.

Complex Services

Overview

For complex services, we implement a hexagonal architecture to our services. We start from the domain/business logic and describe the actual capabilities the service has, as well as defining any dependencies it needs from other domains. The main idea is to drive the development based on the needs of our domain/core business logic, instead of being driven by the dependencies.

Structure Description

ph.safibank.<servicename>

  • core - contains main business logic for service. Code here should have no dependency on any code residing in api/infra, and should be the first part of the code actually done. Components are organized by domain (e.g. account, fraud, etc). Should dependency on any other top level packages except for utils.

    • usecase - contains use case interfaces and classes which define the actual capabilities of the service

    • <domain-package> - contains interfaces and models expected for specific domain (e.g. account, provider, etc)

  • infra - contains actual implementation of the interfaces defined in core. Package structure should be the same as core. Tests should for these classes should be component tests (using test containers/wiremock).

  • api - code for initiating the business logic / use case. Components are organized based on how it’s triggered (REST, messaging, etc). Should only depend on use case interfaces and util classes

  • util - utility code for cross-cutting concerns

Sample package structure

ph.safibank.<name-of-service>
  api
    rest
      UseCaseController.kt
    messaging
      ReponseListener.kt
    ...
  core
    account
      AccountService.kt
    fraud
    ...
    usecase
      impl
        UseCaseImpl.kt
      UseCase.kt
  infra
    account
      AccountServiceImpl.kt
    fraud
    ...
  util
    Util.kt
  Application.kt

Sample code: https://github.com/SafiBank/SaFiMono/tree/main/services/inbound-transaction-manager

Test Driven Development (TDD)

Overview

Test driven development is a programming paradigm wherein you start by creating a test for the functionality before creating the actual implementation. Each test should be small and should validate a specific functionality.

The goal is for the design of our code to be consumer/user focused, since starting from the test places generally places you in the perspective of the user first. This should lead to us creating better designed, maintainable code.

General Steps

We want to follow a red-green-refactor approach when adding new code to our code base:

  1. Create a test to verify a specific functionality

    1. make the test small

    2. only verify what is necessary for that test

    3. make sure the tests can be run independently from one another

  2. Run the test - make sure it fails for expected reasons (RED)

  3. Add the minimum amount of code needed to make the test pass

  4. Run the test again. All tests should pass. (GREEN)

    1. If any of the tests fail, return to step 3.

  5. Once all tests pass, refactor the code. (REFACTOR)

    1. You can refactor either the implementation or the tests, though only do it one at a time.

    2. Run the tests every time you make a change

General Development Cycle

Overview

We want to have the mindset when developing our code base:

  • consumer-driven

    • focused on what the users of our code actually needs

    • focused on what our core business logic actually needs

  • test-driven

    • create more confidence in our code

    • assume the perspective of the consumers by starting with the tests.

As such, we want to adopt a structure that enables both - hexagonal architecture and TDD.

Development Steps

  1. Use TDD for every step

  2. Start from the core

    1. Create a test for a specific use case

    2. Define dependencies as interfaces and mock them with code level mocks in tests

    3. Should have the most amount of tests

  3. Create implementation for dependencies - infra

    1. Use component tests here (wiremock, test containers)

  4. Create API to expose/trigger business logic use cases - api

    1. Should conform to API contracts defined with other consumers

    2. Should have an end-to-end component test to check that wiring is correct

Further Reading

Attachments:

red-green-refactor-tdd.png (image/png)
code-structure-hexagon.png (image/png)
code-structure-hexagon (application/vnd.jgraph.mxfile)