Dependencies

As we would like to easily scale horizontally, there is intention to minimise set of libraries used to lower the size of final docker image. Therefore we prefer to stick to the existing libraries provided by micronaut framework itself. Therefore we do not use for example JPA API nor Hibernate framework, instead we use Micronaut Data. This approach also nicely works with immutable Kotlin data classes.

  • Before including new library, do the homework and check, if there is no functionality in place

  • When including new library verify included transitive using ./gradlew dependencies

Source organization

As mentioned in repository structure, microservices are stored within root services folder. Below this, microservices should be organised as follows:

  • Microservices should follow naming rules (lowercase, separated by commas, suffix ‘manager’ or ‘gateway’)

  • Within microservice folder should be stored local installation of gradle wrapper.

  • There should be just one folder containing source code (src) and below:

    • main - for app sources

      • kotlin - containing kotlin source files

      • resources - non compilable artefacts of resulting JAR

        • application.yml - micronaut configuration without profiles and default values for environment variables.

        • application-init.yml - micronaut configuration for database upgrades with Flyway where the kafka integration is set to false, if temporal is used, it also needs to be disabled (check Init container for more info)

        • db.migration - folder containing migrations for flyway.

        • logback.xml - configuration for microservice logging.

    • test - containing tests which are capable of being run locally without previous configuration. Only exception to this rule is being able to run test containers and therefore having Docker installed. Apart from that ‘test’ folder contains:

      • kotlin - containing tests writen in JUnit 5

      • resources - containing:

        • application-test.yml - micronaut configuration for specific local testing. Default values are allowed here.

    • itest - folder containing integration tests. These test are runnable only in the case, dev environment is reachable, or (significant) local configuration, using fe. docker compose, is needed. Apart from this, for 'itest' applies same rule as for 'test'.

Mentioned structure is the default one, and should be followed, for kotlin & micronaut source code using gradle or even maven tooling. The only exception is the part below named as ‘itest’. This will be described in detail in Gradle separate page.

From source code structuring, recommended package organisation looks like this:

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.

Rest conventions

See REST API guidelines

DB Model conventions

As we are using Kotlin, for model we use certain fields, which are standard across most of the entities. To prevent possible issue with different precision of Instant values, micronaut-data-jdbc of version at least 3.4.2 is required. The fields are (truncatedTo is required to mitigate above mentioned issue):

  • @DateCreated(truncatedTo = ChronoUnit.MILLIS) val createdAt: Instant? = null - describing when entry was written into the DB, this is done automatically

  • val createdBy: String - describing author of the record (TODO, how to get this data)

  • @DateUpdated(truncatedTo = ChronoUnit.MILLIS) val updatedAt: Instant? = null - describing when entry was updated in the DB, this is done automatically

  • val updatedBy: String - describing author of the change (TODO, how to get this data)

  • @field:Version val version: Long? = 0L - field for optimistic locking. This allows RC isolation level and improves performance by lowering amount of DB locks.

Micronaut usage

TODO configuration - mention predefined configs for app/test/itest

describe tx definition

Logging

For logging is used SLF4J library. General rules are:

  • Use log.debug when logging information to analyse bugs in “debug” mode. This log level would not be available in production, however in case it is needed, it could be initiated in production for specific issue analysis.

  • Use log.info when logging ordinary event on controller/service level.

  • Use log.warn logging unexpected, but still manageable situation. Eg. communication is lost, but there is retry routine, or alternative scenario. In this case we also log the exceptions when we are handling them.However try to avoid logging whole stacktrace if not necessary as it might be misleading.

  • Use log.error when the exception leaves the microservice itself. So even when we respond on controller by 40x, we do log the exception, which caused the issue. Do not use this level, when throwing an exception! In such case, there would be chain of the exceptions in the log. However, when consuming the exception, it is necessary to log it, but preferably by log.warn. Apart from error message we log also the exception itself. Eg. log.error("some exception occured", e).

In Kotlin we ca use inline function to ease logging. Currently we are in process of moving it into “common” area, so all can use it.

Error handling

TODO