Sangeetha-Grantha

Metadata Value
Status Active
Version 1.1.0
Last Updated 2026-09-10
Author Sangeetha Grantha Team
Document Type Evidence record

Kotlin Codebase Refactoring Checklist


[!NOTE] Historical evidence: results, counts, commands, and observations below belong to the original work described here. The editorial update date is not a new test or corpus verification. For present behavior, use current quality checks.

Date: 2026-01-27 Scope: modules/backend/ and modules/shared/ Related: kotlin-code-review.md


How to Use This Checklist

Each item includes:

Mark items with:


1. Architecture Improvements

1.1 Service Layer Interfaces

Priority: P0 | Effort: L

Extract interfaces for all service classes to enable testing and flexibility.

Example:

// Before (KrithiService.kt)
class KrithiService(private val dal: SangitaDal) {
    suspend fun search(request: KrithiSearchRequest, publishedOnly: Boolean = true): KrithiSearchResult
}

// After
interface IKrithiService {
    suspend fun search(request: KrithiSearchRequest, publishedOnly: Boolean = true): KrithiSearchResult
    suspend fun getKrithi(id: Uuid): KrithiDto?
    suspend fun createKrithi(request: KrithiCreateRequest): KrithiDto
    suspend fun updateKrithi(id: Uuid, request: KrithiUpdateRequest): KrithiDto
    // ...
}

class KrithiServiceImpl(private val dal: SangitaDal) : IKrithiService {
    // Implementation unchanged
}

Files:


1.2 Decompose BulkImportWorkerService

Priority: P1 | Effort: XL

Split the 847-line god class into focused components.

Target Structure:

services/
├── bulkimport/
│   ├── BulkImportOrchestrator.kt     # Main coordinator
│   ├── ManifestParser.kt             # CSV parsing
│   ├── RateLimiter.kt                # Domain rate limiting
│   ├── TaskDispatcher.kt             # Channel management
│   ├── workers/
│   │   ├── ManifestWorker.kt
│   │   ├── ScrapeWorker.kt
│   │   └── ResolutionWorker.kt
│   └── handlers/
│       ├── BatchCompletionHandler.kt
│       └── TaskErrorHandler.kt

Files:


1.3 Migrate to Koin Dependency Injection

Priority: P2 | Effort: L

Replace manual DI in App.kt with Koin modules.

Example:

// di/AppModule.kt
val appModule = module {
    single { SangitaDal() }
    single<IKrithiService> { KrithiServiceImpl(get()) }
    single<IImportService> { ImportServiceImpl(get()) }
    // ...
}

// App.kt
fun main() {
    startKoin { modules(appModule, dalModule) }
    embeddedServer(Netty, ...) {
        // Dependencies injected via Koin
    }.start(wait = true)
}

Files:


2. Code Quality Fixes

2.1 Replace Magic Numbers with Constants

Priority: P2 | Effort: S

Example:

// Before (KrithiRepository.kt:598)
val safeSize = pageSize.coerceIn(1, 200)

// After
companion object {
    const val MIN_PAGE_SIZE = 1
    const val MAX_PAGE_SIZE = 200
}
val safeSize = pageSize.coerceIn(MIN_PAGE_SIZE, MAX_PAGE_SIZE)

Files:


2.2 Use require/check for Preconditions

Priority: P2 | Effort: S

Example:

// Before (KrithiService.kt:236-241)
private fun parseUuidOrThrow(value: String, label: String): UUID =
    try {
        UUID.fromString(value)
    } catch (ex: IllegalArgumentException) {
        throw IllegalArgumentException("Invalid $label")
    }

// After
private fun parseUuidOrThrow(value: String, label: String): UUID {
    require(value.isNotBlank()) { "$label must not be blank" }
    return runCatching { UUID.fromString(value) }
        .getOrElse { throw IllegalArgumentException("Invalid $label: must be a valid UUID") }
}

Files:


2.3 Extract UUID Parsing Helper

Priority: P3 | Effort: S

Create a reusable extension function for UUID parsing.

Example:

// support/UuidParsing.kt
fun String?.toJavaUuidOrNull(label: String): UUID? = runCatching { this?.let(UUID::fromString) }.getOrNull()

fun String.toJavaUuidOrThrow(label: String): UUID {
    require(this.isNotBlank()) { "$label must not be blank" }
    return UUID.fromString(this)
}

Files:


2.4 Complete TODO Items

Priority: P1 | Effort: M

Example:

// Before
createdByUserId = null, // TODO: Extract from auth context

// After - create UserContextService
class UserContextService(private val call: ApplicationCall) {
    fun currentUserId(): Uuid? = call.principal<JwtPrincipal>()?.userId?.toUuid()
}

// In service
createdByUserId = userContext.currentUserId()

2.5 Reduce Parameter Count

Priority: P3 | Effort: M

Use builder pattern or parameter objects for functions with many parameters.

Example:

// Before
suspend fun create(
    title: String,
    titleNormalized: String,
    incipit: String?,
    // ... 15 more parameters
): KrithiDto

// After
data class KrithiCreateParams(
    val title: String,
    val titleNormalized: String,
    val incipit: String? = null,
    // ... structured with defaults
)

suspend fun create(params: KrithiCreateParams): KrithiDto

Files:


3. Security Hardening

3.1 Implement JWT Authentication

Priority: P0 | Effort: L

Replace single admin token with proper JWT.

Example:

// config/JwtConfig.kt
object JwtConfig {
    val secret = System.getenv("JWT_SECRET") ?: error("JWT_SECRET required")
    val issuer = "sangita-grantha"
    val audience = "sangita-users"
    val realm = "Sangita Grantha API"

    fun generateToken(userId: Uuid, roles: List<String>): String {
        return JWT.create()
            .withIssuer(issuer)
            .withAudience(audience)
            .withClaim("userId", userId.toString())
            .withClaim("roles", roles)
            .withExpiresAt(Date(System.currentTimeMillis() + 3600_000))
            .sign(Algorithm.HMAC256(secret))
    }
}

// plugins/Security.kt
authentication {
    jwt("admin-auth") {
        realm = JwtConfig.realm
        verifier(JWT.require(Algorithm.HMAC256(JwtConfig.secret))
            .withIssuer(JwtConfig.issuer)
            .build())
        validate { credential ->
            if (credential.payload.audience.contains(JwtConfig.audience)) {
                JwtPrincipal(credential.payload)
            } else null
        }
    }
}

Files:


3.2 Move API Key to Headers

Priority: P1 | Effort: S

Remove API key from URL query parameters.

Example:

// Before (GeminiApiClient.kt:51-52)
url("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=$apiKey")

// After
url("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent")
header("x-goog-api-key", apiKey)

Files:


3.3 Add Request Validation Middleware

Priority: P1 | Effort: M

Example:

// plugins/RequestValidation.kt
fun Application.configureRequestValidation() {
    install(RequestValidation) {
        validate<KrithiCreateRequest> { request ->
            if (request.title.isBlank()) {
                ValidationResult.Invalid("Title must not be blank")
            } else {
                ValidationResult.Valid
            }
        }
    }

    install(RateLimit) {
        global {
            rateLimiter(limit = 100, refillPeriod = 1.minutes)
        }
    }
}

Files:


4. Performance Optimizations

4.1 Optimize Search Query (N+1 Fix)

Priority: P1 | Effort: M

Reduce search query from 3 queries to 1 using proper JOINs.

Example:

// Before: 3 separate queries
val krithiDtos = baseQuery.map { it.toKrithiDto() }
val composersMap = ComposersTable.selectAll()...
val ragasMap = KrithiRagasTable.join(RagasTable)...

// After: Single query with JOINs
val query = KrithisTable
    .join(ComposersTable, JoinType.INNER, KrithisTable.composerId, ComposersTable.id)
    .leftJoin(KrithiRagasTable, { KrithisTable.id }, { KrithiRagasTable.krithiId })
    .leftJoin(RagasTable, { KrithiRagasTable.ragaId }, { RagasTable.id })
    .select(
        KrithisTable.columns + ComposersTable.name + RagasTable.id + RagasTable.name + KrithiRagasTable.orderIndex
    )

Files:


4.2 Add Response Caching

Priority: P2 | Effort: M

Cache frequently accessed reference data.

Example:

// plugins/Caching.kt
fun Application.configureCaching() {
    install(CachingHeaders) {
        options { call, outgoingContent ->
            when (call.request.uri) {
                in listOf("/v1/composers", "/v1/ragas", "/v1/talas") ->
                    CachingOptions(CacheControl.MaxAge(maxAgeSeconds = 3600))
                else -> null
            }
        }
    }
}

Files:


4.3 Add Database Query Logging

Priority: P3 | Effort: S

Enable query logging for performance analysis.

Example:

// DatabaseFactory.kt
fun enableQueryLogging() {
    TransactionManager.current().warnLongQueriesDuration = 100 // ms
    addLogger(StdOutSqlLogger)
}

Files:


5. Testing Infrastructure

5.1 Create Test Database Factory

Priority: P1 | Effort: M

Example:

// test/support/TestDatabaseFactory.kt
object TestDatabaseFactory {
    fun connectTestDb() {
        DatabaseFactory.connect(
            databaseUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;MODE=PostgreSQL",
            username = "sa",
            password = "",
            driverClassName = "org.h2.Driver"
        )
        // Run migrations
        transaction {
            SchemaUtils.create(
                ComposersTable, RagasTable, TalasTable, KrithisTable, // ...
            )
        }
    }
}

Files:


5.2 Add Service Unit Tests

Priority: P1 | Effort: L

Test Structure:

class KrithiServiceTest {
    private lateinit var dal: SangitaDal
    private lateinit var service: KrithiService

    @BeforeEach
    fun setup() {
        TestDatabaseFactory.connectTestDb()
        dal = SangitaDal()
        service = KrithiService(dal)
    }

    @Test
    fun `search returns paginated results`() = runTest {
        // Given
        val request = KrithiSearchRequest(query = "test", page = 0, pageSize = 10)

        // When
        val result = service.search(request)

        // Then
        assertThat(result.page).isEqualTo(0)
        assertThat(result.pageSize).isEqualTo(10)
    }
}

Files:


6. Documentation

6.1 Add KDoc Comments

Priority: P2 | Effort: M

Example:

/**
 * Searches for krithis matching the given criteria.
 *
 * @param request Search parameters including filters and pagination
 * @param publishedOnly If true, only returns PUBLISHED krithis (default for public API)
 * @return Paginated search results with krithi summaries
 * @throws IllegalArgumentException if page or pageSize are invalid
 */
suspend fun search(request: KrithiSearchRequest, publishedOnly: Boolean = true): KrithiSearchResult

Files:


6.2 Add Architecture Decision Records

Priority: P3 | Effort: S

Files:


7. Observability

7.1 Add Structured Logging

Priority: P2 | Effort: M

Example:

// config/LogbackConfig.kt
fun configure(env: ApiEnvironment) {
    if (env.environment == "PROD") {
        val encoder = LogstashEncoder()
        // Configure JSON output
    }
}

// MDC in routes
withContext(MDCContext("requestId" to call.callId.toString())) {
    // Request handling
}

Files:


7.2 Add Metrics Collection

Priority: P3 | Effort: M

Example:

// plugins/Metrics.kt
fun Application.configureMetrics() {
    install(MicrometerMetrics) {
        registry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)
        meterBinders = listOf(
            JvmMemoryMetrics(),
            JvmGcMetrics(),
            ProcessorMetrics()
        )
    }
}

Files:


8. Shared Module Improvements

8.1 Add Validation Annotations

Priority: P2 | Effort: S

Example:

@Serializable
data class KrithiSearchRequest(
    val query: String? = null,
    @SerialName("page")
    val page: Int = 0,  // Add: @Min(0)
    @SerialName("pageSize")
    val pageSize: Int = 50,  // Add: @Min(1) @Max(200)
)

Files:


8.2 Split Large DTO Files

Priority: P3 | Effort: S

Target Structure:

domain/model/
├── krithi/
│   ├── KrithiDto.kt
│   ├── KrithiSearchDto.kt
│   ├── KrithiNotationDto.kt
│   └── KrithiEnums.kt
└── import/
    ├── ImportDto.kt
    ├── BatchDto.kt
    └── ImportEnums.kt

Files:


Progress Tracking

Sprint 1 (Security & Critical)

| Item | Status | Assignee | Notes | |——|——–|———-|——-| | 3.1 JWT Authentication | [x] | Sangeetha Grantha Architect | JWT + refresh + claims wired | | 3.2 API Key Headers | [x] | Sangeetha Grantha Architect | Header-based auth + redacted logging | | 1.1 Service Interfaces | [x] | Sangeetha Grantha Architect | Interfaces + impls + DI wiring | | 2.4 Complete TODOs | [x] | Sangeetha Grantha Architect | Auth user context + validation endpoint |

Sprint 2 (Performance & Quality)

| Item | Status | Assignee | Notes | |——|——–|———-|——-| | 4.1 Optimize Search | [x] | Sangeetha Grantha Architect | JOIN rewrite + indexes (materialized view deferred) | | 2.1 Magic Numbers | [x] | Sangeetha Grantha Architect | Constants centralized | | 2.2 Preconditions | [x] | Sangeetha Grantha Architect | Require/check helpers applied | | 5.1 Test Database | [x] | Sangeetha Grantha Architect | H2 factory + fixtures |

Sprint 3 (Architecture & Maintenance)

| Item | Status | Assignee | Notes | |——|——–|———-|——-| | 1.2 Decompose Workers | [x] | Sangeetha Grantha Architect | Bulk import split into components | | 1.3 Koin DI | [x] | Sangeetha Grantha Architect | Koin modules + App.kt wiring | | 6.1 KDoc Comments | [x] | Sangeetha Grantha Architect | Services + repositories + packages | | 4.2 Response Caching | [x] | Sangeetha Grantha Architect | Cache headers + ETag |


Completion Criteria

A refactoring item is considered complete when:

  1. Code changes are implemented
  2. Existing tests pass
  3. New tests are added (if applicable)
  4. Code review approved
  5. Documentation updated
  6. No regression in functionality

Checklist generated from automated code review. Update status as items are completed.


Section index · Documentation home · Feature status