Last Week
A difficult decision was made: I archived the old repository and started fresh.
Before this decision, I had a working product. Users could download the app, go through the entire flow, and it would work perfectly – as long as they stayed on the happy path. But during bug squashing, I discovered a horrifying pattern: fixing one bug often revealed two more. The technical debt from shortcuts taken to rush the MVP was demanding payment with considerable interest.
The Cost of Shortcuts
Was the previous 29 weeks of development wasted? Perhaps. But I prefer to think of it as expensive tuition paid to the gods of bad architecture and insufficient testing. The lessons learned from that experience are now driving a completely different approach.
The new repository isn’t just a fresh start – it’s built on a foundation of strict discipline and automated safeguards designed to prevent me (and any AI coding assistants) from making the same mistakes again.
What does it mean in English?
Imagine building a house quickly by skipping inspections and cutting corners on the foundation. It looks fine at first, but when problems start appearing, fixing them often causes new issues elsewhere. Eventually, it becomes cheaper and faster to tear it down and rebuild properly than to keep patching problems.
That’s exactly what happened with Shokken. The time spent fixing bugs in the poorly architected codebase was exceeding what it would take to rebuild with proper structure.
Nerdy Details
The new architecture enforces discipline through automation at every level:
Test-Driven Development Enforcement
Using Konsist rules in pre-commit hooks, the new codebase enforces:
- 95% test coverage for business logic
- 90% test coverage for all other components
- No commits without tests
// konsist.gradle.kts
konsist {
test("Domain layer must have 95% coverage") {
files
.withNameEndingWith("UseCase")
.assert { it.hasTestCoverage(0.95) }
}
test("Clean architecture layers") {
files.assert {
when {
it.resideInPackage("..domain..") ->
!it.hasImport { import ->
import.contains("data") || import.contains("presentation")
}
it.resideInPackage("..data..") ->
!it.hasImport { import -> import.contains("presentation") }
else -> true
}
}
}
}
Multi-Layer Quality Gates
- Pre-commit hooks: Ktlint, Detekt, and Konsist rules
- Pre-push hooks: Full test suite and build verification
- GitHub Actions: Complete CI/CD pipeline with duplicate checks
# .githooks/pre-push
#!/bin/bash
echo "Running full test suite..."
./gradlew test
echo "Running integration tests..."
./gradlew connectedAndroidTest
echo "Verifying build..."
./gradlew assembleDebug
if [ $? -ne 0 ]; then
echo "Push rejected: Tests or build failed"
exit 1
fi
Clean Architecture Implementation
Unlike the previous attempt, the domain layer is now mandatory and properly isolated:
// Before: Everything in ViewModel
class DashboardViewModel : ViewModel() {
fun addCustomer(input: CustomerInput) {
// 200+ lines of mixed concerns
}
}
// Now: Proper separation
// Domain Layer
class AddCustomerUseCase(
private val repository: CustomerRepository,
private val validator: CustomerValidator
) {
suspend operator fun invoke(input: CustomerInput): Result<Customer>
}
// Presentation Layer
class DashboardViewModel(
private val addCustomer: AddCustomerUseCase
) : ViewModel() {
fun onAddCustomerClick(input: CustomerInput) {
viewModelScope.launch {
addCustomer(input).fold(
onSuccess = { updateState(it) },
onFailure = { showError(it) }
)
}
}
}
Trunk-Based Development
The switch from Git Flow to Trunk-Based Development (TBD) represents a fundamental shift in development philosophy:
Git Flow:
master ─────────────────────────────────
\ /
develop ──────────────────────
\ / \ /
feature-1 feature-2
Trunk-Based Development:
main ──●──●──●──●──●──●──●──●──●──●──
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
Small, tested, complete commits
Key TBD principles implemented:
- All commits go directly to main branch
- Every commit must pass all quality gates
- No long-lived feature branches
- Pull requests merged same day or deleted
Platform Parity
The new approach develops for all platforms simultaneously:
// Common code in shared module
expect class NotificationService {
fun scheduleNotification(customer: Customer)
}
// Android implementation
actual class NotificationService {
actual fun scheduleNotification(customer: Customer) {
// Android-specific implementation
}
}
// iOS implementation
actual class NotificationService {
actual fun scheduleNotification(customer: Customer) {
// iOS-specific implementation
}
}
Automated Deployment
The CI/CD pipeline now includes:
- Automated Android builds and Google Play uploads
- iOS build preparation (TestFlight coming soon)
- Environment-specific configurations
- Automatic versioning based on commit count
# .github/workflows/deploy.yml
- name: Build and Deploy
run: |
VERSION_CODE=$(git rev-list --count HEAD)
./gradlew bundleRelease \
-PversionCode=$VERSION_CODE \
-PversionName="1.0.$VERSION_CODE"
# Upload to Google Play
./gradlew publishReleaseBundle
Next Week
With the foundation in place, next week focuses on implementing the clean architecture scaffolding:
- Setting up the domain, data, and presentation layers
- Integrating Orbit MVI for state management
- Creating the initial UI storyboards with proper architectural boundaries
The goal isn’t speed – it’s sustainability. Every feature will be built properly from the start, tested thoroughly, and implemented across all platforms before moving to the next one.