{code}

Greenfield software development :-)

# CODE
  • Clean architecture
  • Organizing code
  • Lit frontend in separate repo
  • Kotlin
  • Liquibase
  • Testcontainers
  • AsciiDoc and Confluence

Agenda

# ORGANIZING CODE

Clean Architecture

Dependency rule:

 

source code dependencies can only point inwards

# ORGANIZING CODE

Grouping source code in

  • Git repositories
  • Maven modules
  • Java packages

Maven modules

# ORGANIZING CODE
  • Multi-Module project
  • Module dependencies enforce Clean Architecture
    dependencies can only point downwards
  • Spring Dependency Injection

Maven modules

# ORGANIZING CODE

Lit frontend code in separate repo

P08389-CPCM-frontend
Javascript code

cpcm-frontend-0.0.12.jar

P08389-CPCM
Java/Kotlin code

P08389-outgoing-frontend

P02733-incoming-bmg

<dependency>
  <groupId>nl.mendesgans.cpcm</groupId>
  <artifactId>cpcm-frontend</artifactId>
  <version>0.0.12</version>
</dependency>
# KOTLIN

Kotlin

# KOTLIN

Kotlin is 100% interoperable with Java

compile

*.kt

*.java

Bytecode

decompile

compile

decompile

JVM
jre 1.8+

kotlinc
javac
// MyClass.java
public class MyClass {
    public static void main(final String[] args) throws Exception {
        System.out.println("Hello world!");
    }
}
# KOTLIN
// MyClass.kt
fun main(args: Array<String>) =
    println("Hello world!")

Kotlin

// MyClass.java
public class MyClass {
    private static final String DASHES = "--";
    public static void main(final String... args) {
        new MyClass()
                .run("Hello", text -> System.out.println(DASHES + " " + text + " "))
                .run("world!", text -> System.out.println(text));
    }
    private MyClass run(final String text, final Consumer<String> task) {
        task.accept(text);
        return this;
    }
}
# KOTLIN
// MyClass.kt
fun main(vararg args: String) =
    MyClass()
        .run("Hello", { text -> println("$DASHES $text ") })
        .run("world!") { println(it) }

class MyClass {
    companion object {
        const val DASHES = "--"
    }
    fun run(text: String, task: Consumer<String>): MyClass {
        task.accept(text)
        return this
    }
}
# ARCHITECTURE
// entity
open class BasicRate {
    lateinit var identifier: String
    val lifeSpan = LifeSpan()
    val validity = Validity()
    lateinit var currencyCode: CurrencyCode
    lateinit var rateType: InterestRateType
    var bidRate: BigDecimal? = null
    var askRate: BigDecimal? = null
    var midRate: BigDecimal? = null
}

interface BasicRateFactory {
    fun fetchAll(): Collection<BasicRate>
    fun store(modifiedBasicRates: Collection<BasicRate>)
}

Dependency inversion

// use case
@Service
class DataConsistencyService(
    private val basicRateFactory: BasicRateFactory
) {
    fun verify() =
        basicRateFactory.fetchAll()
            .map { it.validity }
            .sortedBy { it.validFrom }
            .zipWithNext()
            .filter { it.first.overlapsWith(it.second) }
            .map { "BasicRate of ${it.first.validFrom} overlaps with ${it.second.validFrom}" }
}
# ARCHITECTURE
// persistency
@Entity
@Table(schema = "CPCM", name = "BasicRate")
internal class DbBasicRate {
    @Id
    lateinit var identifier: String
    lateinit var validFrom: LocalDate
    var validTo: LocalDate? = null
    @Column(columnDefinition = "CHAR(3)")
    lateinit var currencyCode: CurrencyCode
    @Column(precision = RATE_PRECISION, scale = RATE_SCALE)
    var bidRate: BigDecimal? = null
    // ...
}

Spring dependency injection inversion

@Repository
private interface DbBasicRateRepository : JpaRepository<DbBasicRate, Int>
@Component
private class DbBasicRateFactory(
    private val dbBasicRateRepository: DbBasicRateRepository
) : BasicRateFactory {
    override fun fetchAll() =
        dbBasicRateRepository.findAll()
            .map(DbBasicRateAdapter::toBasicRate)
    // ...
}
# ARCHITECTURE
// persistency
internal object DbBasicRateAdapter {

    fun toDbBasicRate(basicRate: BasicRate) =
        DbBasicRate().apply {
            identifier = basicRate.identifier
            createdBy = basicRate.lifeSpan.createdBy
            createdAt = basicRate.lifeSpan.createdAt
            terminatedAt = basicRate.lifeSpan.terminatedAt
            validFrom = basicRate.validity.validFrom
            validTo = basicRate.validity.validTo
            currencyCode = basicRate.currencyCode
            rateType = basicRate.rateType
            // ...
        }

    fun toBasicRate(dbBasicRate: DbBasicRate) =
        BasicRate().apply {
            identifier = dbBasicRate.identifier
            lifeSpan.createdBy = dbBasicRate.createdBy
            lifeSpan.createdAt = dbBasicRate.createdAt
            lifeSpan.terminatedAt = dbBasicRate.terminatedAt
            validity.validFrom = dbBasicRate.validFrom
            validity.validTo = dbBasicRate.validTo
            currencyCode = dbBasicRate.currencyCode
            // ...
        }
}

Adapters

# LIQUIBASE

Liquibase

liquibase-core-4.17.2.jar

Liquibase wrapper class
fun main()

Maven

# LIQUIBASE

Liquibase

Liquibase wrapper class

CPCM application

Unit Integration Test
Testcontainers

# TESTCONTAINERS

Testcontainers

internal class DbContainer private constructor() :
    MSSQLServerContainer<DbContainer>("mcr.microsoft.com/mssql/server:latest") {

    companion object {
        val singleton by lazy { DbContainer() }
    }

    override fun start() {
        acceptLicense()
        // Start the Docker container.
        super.start()
        // Create database 'NCP' and then update schema 'CPCM' using Liquibase.
        createDatabase(jdbcUrl, username, password) // DbContainer.username = "sa"
        updateSchema(jdbcUrl, DBO_USER, DBO_PASSWORD, "UNIT_TEST")
    }
}
# TESTCONTAINERS

Testcontainers

// This extension must be triggered BEFORE any extension that requires database access.
private class StartTestcontainersBeforeAll : BeforeAllCallback {

    override fun beforeAll(extensionContext: ExtensionContext) {
        // Start the database in a Docker container if it is not already running.
        if (!DatabaseContainer.singleton.isRunning) {
            DatabaseContainer.singleton.start()
        }
    }
}

@SpringBootTest(classes = [CPCMApplication::class])
@TestPropertySource("classpath:application-test.properties")
@ExtendWith(StartTestcontainersBeforeAll::class)
interface AbstractTestcontainersIT {

    companion object {
        @DynamicPropertySource
        @JvmStatic
        fun applyContainerPropertiesToSpring(registry: DynamicPropertyRegistry) {
            registry.add("spring.datasource.url") { DatabaseContainer.singleton.jdbcUrl }
            registry.add("spring.datasource.username") { DB_USER }
            registry.add("spring.datasource.password") { DB_PASSWORD }
        }
    }
}
# ASCIIDOC

AsciiDoc and Confluence

  • Write documentation 'as source code'
    Stored in git repo, version controlled
     
  • Use AsciiDoc format
    Portable, rich text formatting, supports images and attachments
     
  • IntelliJ plugin
     
  • Publish to Confluence
    Ad-hoc, Maven plugin

Code

By Rob Bosman

Code

  • 82