{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