agentic-team-templates 0.13.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/package.json +1 -1
- package/src/index.js +91 -13
- package/src/index.test.js +95 -1
- package/templates/cpp-expert/.cursorrules/concurrency.md +211 -0
- package/templates/cpp-expert/.cursorrules/error-handling.md +170 -0
- package/templates/cpp-expert/.cursorrules/memory-and-ownership.md +220 -0
- package/templates/cpp-expert/.cursorrules/modern-cpp.md +211 -0
- package/templates/cpp-expert/.cursorrules/overview.md +87 -0
- package/templates/cpp-expert/.cursorrules/performance.md +223 -0
- package/templates/cpp-expert/.cursorrules/testing.md +230 -0
- package/templates/cpp-expert/.cursorrules/tooling.md +312 -0
- package/templates/cpp-expert/CLAUDE.md +242 -0
- package/templates/csharp-expert/.cursorrules/aspnet-core.md +311 -0
- package/templates/csharp-expert/.cursorrules/async-patterns.md +206 -0
- package/templates/csharp-expert/.cursorrules/dependency-injection.md +206 -0
- package/templates/csharp-expert/.cursorrules/error-handling.md +235 -0
- package/templates/csharp-expert/.cursorrules/language-features.md +204 -0
- package/templates/csharp-expert/.cursorrules/overview.md +92 -0
- package/templates/csharp-expert/.cursorrules/performance.md +251 -0
- package/templates/csharp-expert/.cursorrules/testing.md +282 -0
- package/templates/csharp-expert/.cursorrules/tooling.md +254 -0
- package/templates/csharp-expert/CLAUDE.md +360 -0
- package/templates/java-expert/.cursorrules/concurrency.md +209 -0
- package/templates/java-expert/.cursorrules/error-handling.md +205 -0
- package/templates/java-expert/.cursorrules/modern-java.md +216 -0
- package/templates/java-expert/.cursorrules/overview.md +81 -0
- package/templates/java-expert/.cursorrules/performance.md +239 -0
- package/templates/java-expert/.cursorrules/persistence.md +262 -0
- package/templates/java-expert/.cursorrules/spring-boot.md +262 -0
- package/templates/java-expert/.cursorrules/testing.md +272 -0
- package/templates/java-expert/.cursorrules/tooling.md +301 -0
- package/templates/java-expert/CLAUDE.md +325 -0
- package/templates/javascript-expert/.cursorrules/overview.md +5 -3
- package/templates/javascript-expert/.cursorrules/typescript-deep-dive.md +348 -0
- package/templates/javascript-expert/CLAUDE.md +34 -3
- package/templates/kotlin-expert/.cursorrules/coroutines.md +237 -0
- package/templates/kotlin-expert/.cursorrules/error-handling.md +149 -0
- package/templates/kotlin-expert/.cursorrules/frameworks.md +227 -0
- package/templates/kotlin-expert/.cursorrules/language-features.md +231 -0
- package/templates/kotlin-expert/.cursorrules/overview.md +77 -0
- package/templates/kotlin-expert/.cursorrules/performance.md +185 -0
- package/templates/kotlin-expert/.cursorrules/testing.md +213 -0
- package/templates/kotlin-expert/.cursorrules/tooling.md +258 -0
- package/templates/kotlin-expert/CLAUDE.md +276 -0
- package/templates/swift-expert/.cursorrules/concurrency.md +230 -0
- package/templates/swift-expert/.cursorrules/error-handling.md +213 -0
- package/templates/swift-expert/.cursorrules/language-features.md +246 -0
- package/templates/swift-expert/.cursorrules/overview.md +88 -0
- package/templates/swift-expert/.cursorrules/performance.md +260 -0
- package/templates/swift-expert/.cursorrules/swiftui.md +260 -0
- package/templates/swift-expert/.cursorrules/testing.md +286 -0
- package/templates/swift-expert/.cursorrules/tooling.md +285 -0
- package/templates/swift-expert/CLAUDE.md +275 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# Kotlin Error Handling
|
|
2
|
+
|
|
3
|
+
Sealed types for expected failures. Exceptions for exceptional conditions. Never swallow errors.
|
|
4
|
+
|
|
5
|
+
## Sealed Result Types
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
// Model expected outcomes explicitly
|
|
9
|
+
sealed interface Result<out T> {
|
|
10
|
+
data class Success<T>(val value: T) : Result<T>
|
|
11
|
+
data class Failure(val error: AppError) : Result<Nothing>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
sealed interface AppError {
|
|
15
|
+
data class NotFound(val entity: String, val id: String) : AppError
|
|
16
|
+
data class Validation(val errors: Map<String, String>) : AppError
|
|
17
|
+
data class Conflict(val message: String) : AppError
|
|
18
|
+
data class Unauthorized(val reason: String) : AppError
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Usage in services
|
|
22
|
+
class UserService(private val userRepo: UserRepository) {
|
|
23
|
+
suspend fun register(request: CreateUserRequest): Result<User> {
|
|
24
|
+
val errors = validate(request)
|
|
25
|
+
if (errors.isNotEmpty()) {
|
|
26
|
+
return Result.Failure(AppError.Validation(errors))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
val existing = userRepo.findByEmail(request.email)
|
|
30
|
+
if (existing != null) {
|
|
31
|
+
return Result.Failure(AppError.Conflict("Email already registered"))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
val user = userRepo.create(request)
|
|
35
|
+
return Result.Success(user)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Handling in routes/controllers
|
|
40
|
+
when (val result = userService.register(request)) {
|
|
41
|
+
is Result.Success -> call.respond(HttpStatusCode.Created, result.value)
|
|
42
|
+
is Result.Failure -> when (result.error) {
|
|
43
|
+
is AppError.Validation -> call.respond(HttpStatusCode.BadRequest, result.error)
|
|
44
|
+
is AppError.Conflict -> call.respond(HttpStatusCode.Conflict, result.error)
|
|
45
|
+
is AppError.NotFound -> call.respond(HttpStatusCode.NotFound, result.error)
|
|
46
|
+
is AppError.Unauthorized -> call.respond(HttpStatusCode.Unauthorized, result.error)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## kotlin.Result and runCatching
|
|
52
|
+
|
|
53
|
+
```kotlin
|
|
54
|
+
// For wrapping operations that might throw
|
|
55
|
+
val result: kotlin.Result<User> = runCatching {
|
|
56
|
+
userRepository.findById(id)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result
|
|
60
|
+
.onSuccess { user -> logger.info("Found user: ${user.id}") }
|
|
61
|
+
.onFailure { error -> logger.error("Failed to find user", error) }
|
|
62
|
+
|
|
63
|
+
val user = result.getOrNull()
|
|
64
|
+
val userOrDefault = result.getOrDefault(User.anonymous())
|
|
65
|
+
val userOrElse = result.getOrElse { error ->
|
|
66
|
+
logger.warn("Falling back to anonymous", error)
|
|
67
|
+
User.anonymous()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Chaining
|
|
71
|
+
val displayName = runCatching { fetchUser(id) }
|
|
72
|
+
.map { it.displayName }
|
|
73
|
+
.getOrDefault("Unknown User")
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Validation
|
|
77
|
+
|
|
78
|
+
```kotlin
|
|
79
|
+
// require — preconditions (throws IllegalArgumentException)
|
|
80
|
+
fun createOrder(customerId: String, items: List<OrderItem>) {
|
|
81
|
+
require(customerId.isNotBlank()) { "customerId must not be blank" }
|
|
82
|
+
require(items.isNotEmpty()) { "Order must have at least one item" }
|
|
83
|
+
require(items.size <= MAX_ITEMS) { "Maximum $MAX_ITEMS items allowed" }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// check — state invariants (throws IllegalStateException)
|
|
87
|
+
fun ship(order: Order) {
|
|
88
|
+
check(order.status == OrderStatus.PAID) { "Can only ship paid orders" }
|
|
89
|
+
check(order.items.isNotEmpty()) { "Cannot ship empty order" }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// requireNotNull / checkNotNull
|
|
93
|
+
val user = requireNotNull(userRepo.findById(id)) {
|
|
94
|
+
"User $id must exist after authentication"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Exception Best Practices
|
|
99
|
+
|
|
100
|
+
```kotlin
|
|
101
|
+
// Custom exceptions for domain boundaries
|
|
102
|
+
class OrderProcessingException(
|
|
103
|
+
val orderId: OrderId,
|
|
104
|
+
message: String,
|
|
105
|
+
cause: Throwable? = null
|
|
106
|
+
) : RuntimeException(message, cause)
|
|
107
|
+
|
|
108
|
+
// Catch specific exceptions
|
|
109
|
+
try {
|
|
110
|
+
paymentService.charge(order)
|
|
111
|
+
} catch (e: PaymentDeclinedException) {
|
|
112
|
+
logger.warn("Payment declined for order ${order.id}", e)
|
|
113
|
+
return Result.Failure(AppError.PaymentDeclined(e.reason))
|
|
114
|
+
} catch (e: PaymentTimeoutException) {
|
|
115
|
+
logger.error("Payment timeout for order ${order.id}", e)
|
|
116
|
+
throw OrderProcessingException(order.id, "Payment timed out", e)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Never catch Exception broadly without rethrowing CancellationException
|
|
120
|
+
try {
|
|
121
|
+
suspendingOperation()
|
|
122
|
+
} catch (e: CancellationException) {
|
|
123
|
+
throw e // Always rethrow — coroutine cancellation must propagate
|
|
124
|
+
} catch (e: Exception) {
|
|
125
|
+
logger.error("Operation failed", e)
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Anti-Patterns
|
|
130
|
+
|
|
131
|
+
```kotlin
|
|
132
|
+
// Never: empty catch blocks
|
|
133
|
+
try { riskyOperation() }
|
|
134
|
+
catch (e: Exception) { } // Silently swallowed
|
|
135
|
+
|
|
136
|
+
// Never: catch Throwable (catches OutOfMemoryError, StackOverflowError)
|
|
137
|
+
try { work() }
|
|
138
|
+
catch (e: Throwable) { } // Too broad
|
|
139
|
+
// Use: catch (e: Exception) at most
|
|
140
|
+
|
|
141
|
+
// Never: using exceptions for control flow
|
|
142
|
+
try { return items.first { it.isActive } }
|
|
143
|
+
catch (e: NoSuchElementException) { return null }
|
|
144
|
+
// Use: items.firstOrNull { it.isActive }
|
|
145
|
+
|
|
146
|
+
// Never: wrapping without context
|
|
147
|
+
catch (e: Exception) { throw RuntimeException(e) }
|
|
148
|
+
// Add context: throw OrderProcessingException(orderId, "Failed to process", e)
|
|
149
|
+
```
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# Kotlin Frameworks
|
|
2
|
+
|
|
3
|
+
Ktor for Kotlin-first, Spring Boot for ecosystem breadth. Both done idiomatically.
|
|
4
|
+
|
|
5
|
+
## Ktor
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
fun main() {
|
|
9
|
+
embeddedServer(Netty, port = 8080) {
|
|
10
|
+
configureRouting()
|
|
11
|
+
configureSerialization()
|
|
12
|
+
configureAuthentication()
|
|
13
|
+
}.start(wait = true)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Routing
|
|
17
|
+
fun Application.configureRouting() {
|
|
18
|
+
routing {
|
|
19
|
+
route("/api/v1") {
|
|
20
|
+
userRoutes()
|
|
21
|
+
orderRoutes()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fun Route.userRoutes() {
|
|
27
|
+
route("/users") {
|
|
28
|
+
get {
|
|
29
|
+
val users = userService.findAll()
|
|
30
|
+
call.respond(users)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
get("/{id}") {
|
|
34
|
+
val id = UserId(call.parameters["id"]
|
|
35
|
+
?: return@get call.respond(HttpStatusCode.BadRequest, "Missing id"))
|
|
36
|
+
|
|
37
|
+
when (val result = userService.findById(id)) {
|
|
38
|
+
is Result.Success -> call.respond(result.value)
|
|
39
|
+
is Result.Failure -> when (result.error) {
|
|
40
|
+
is AppError.NotFound -> call.respond(HttpStatusCode.NotFound)
|
|
41
|
+
else -> call.respond(HttpStatusCode.InternalServerError)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
post {
|
|
47
|
+
val request = call.receive<CreateUserRequest>()
|
|
48
|
+
when (val result = userService.create(request)) {
|
|
49
|
+
is Result.Success -> call.respond(HttpStatusCode.Created, result.value)
|
|
50
|
+
is Result.Failure -> call.respond(HttpStatusCode.BadRequest, result.error)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Content negotiation
|
|
57
|
+
fun Application.configureSerialization() {
|
|
58
|
+
install(ContentNegotiation) {
|
|
59
|
+
json(Json {
|
|
60
|
+
prettyPrint = false
|
|
61
|
+
ignoreUnknownKeys = true
|
|
62
|
+
encodeDefaults = true
|
|
63
|
+
isLenient = false
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Spring Boot with Kotlin
|
|
70
|
+
|
|
71
|
+
```kotlin
|
|
72
|
+
@SpringBootApplication
|
|
73
|
+
class Application
|
|
74
|
+
|
|
75
|
+
fun main(args: Array<String>) {
|
|
76
|
+
runApplication<Application>(*args)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// REST controller — Kotlin-idiomatic
|
|
80
|
+
@RestController
|
|
81
|
+
@RequestMapping("/api/v1/orders")
|
|
82
|
+
class OrderController(private val orderService: OrderService) {
|
|
83
|
+
|
|
84
|
+
@GetMapping("/{id}")
|
|
85
|
+
suspend fun getById(@PathVariable id: UUID): ResponseEntity<OrderResponse> {
|
|
86
|
+
return orderService.findById(OrderId(id.toString()))
|
|
87
|
+
?.let { ResponseEntity.ok(OrderResponse.from(it)) }
|
|
88
|
+
?: ResponseEntity.notFound().build()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
@PostMapping
|
|
92
|
+
suspend fun create(@Valid @RequestBody request: CreateOrderRequest): ResponseEntity<OrderResponse> {
|
|
93
|
+
return when (val result = orderService.create(request)) {
|
|
94
|
+
is Result.Success -> ResponseEntity
|
|
95
|
+
.created(URI("/api/v1/orders/${result.value.id}"))
|
|
96
|
+
.body(OrderResponse.from(result.value))
|
|
97
|
+
is Result.Failure -> ResponseEntity
|
|
98
|
+
.badRequest()
|
|
99
|
+
.build()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Configuration with Kotlin DSL
|
|
105
|
+
@Configuration
|
|
106
|
+
class SecurityConfig {
|
|
107
|
+
@Bean
|
|
108
|
+
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
|
|
109
|
+
return http {
|
|
110
|
+
csrf { disable() }
|
|
111
|
+
authorizeHttpRequests {
|
|
112
|
+
authorize("/api/public/**", permitAll)
|
|
113
|
+
authorize("/actuator/health", permitAll)
|
|
114
|
+
authorize(anyRequest, authenticated)
|
|
115
|
+
}
|
|
116
|
+
oauth2ResourceServer { jwt { } }
|
|
117
|
+
sessionManagement { sessionCreationPolicy = SessionCreationPolicy.STATELESS }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Exposed (Kotlin SQL Framework)
|
|
124
|
+
|
|
125
|
+
```kotlin
|
|
126
|
+
// Type-safe SQL — no string queries
|
|
127
|
+
object Users : Table("users") {
|
|
128
|
+
val id = uuid("id").autoGenerate()
|
|
129
|
+
val name = varchar("name", 200)
|
|
130
|
+
val email = varchar("email", 255).uniqueIndex()
|
|
131
|
+
val active = bool("active").default(true)
|
|
132
|
+
val createdAt = timestamp("created_at").defaultExpression(CurrentTimestamp)
|
|
133
|
+
|
|
134
|
+
override val primaryKey = PrimaryKey(id)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Repository
|
|
138
|
+
class UserRepository(private val database: Database) {
|
|
139
|
+
|
|
140
|
+
suspend fun findByEmail(email: String): User? = dbQuery {
|
|
141
|
+
Users.selectAll()
|
|
142
|
+
.where { Users.email eq email }
|
|
143
|
+
.map { it.toUser() }
|
|
144
|
+
.singleOrNull()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
suspend fun create(request: CreateUserRequest): User = dbQuery {
|
|
148
|
+
val id = Users.insert {
|
|
149
|
+
it[name] = request.name
|
|
150
|
+
it[email] = request.email
|
|
151
|
+
} get Users.id
|
|
152
|
+
|
|
153
|
+
findById(UserId(id.toString()))!!
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private suspend fun <T> dbQuery(block: suspend () -> T): T =
|
|
157
|
+
newSuspendedTransaction(Dispatchers.IO) { block() }
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Ktor Client (HTTP)
|
|
162
|
+
|
|
163
|
+
```kotlin
|
|
164
|
+
// Shared client instance with configuration
|
|
165
|
+
val httpClient = HttpClient(CIO) {
|
|
166
|
+
install(ContentNegotiation) { json() }
|
|
167
|
+
install(HttpTimeout) {
|
|
168
|
+
requestTimeoutMillis = 30_000
|
|
169
|
+
connectTimeoutMillis = 5_000
|
|
170
|
+
}
|
|
171
|
+
install(HttpRequestRetry) {
|
|
172
|
+
retryOnServerErrors(maxRetries = 3)
|
|
173
|
+
exponentialDelay()
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Type-safe API calls
|
|
178
|
+
suspend fun fetchGitHubUser(username: String): GitHubUser {
|
|
179
|
+
return httpClient.get("https://api.github.com/users/$username").body()
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Dependency Injection (Koin)
|
|
184
|
+
|
|
185
|
+
```kotlin
|
|
186
|
+
// Module definition
|
|
187
|
+
val appModule = module {
|
|
188
|
+
single { Database.connect(get<DatabaseConfig>().url) }
|
|
189
|
+
single { UserRepository(get()) }
|
|
190
|
+
single { OrderRepository(get()) }
|
|
191
|
+
factory { UserService(get(), get()) }
|
|
192
|
+
factory { OrderService(get(), get()) }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ktor integration
|
|
196
|
+
fun Application.configureKoin() {
|
|
197
|
+
install(Koin) {
|
|
198
|
+
modules(appModule)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Injection in routes
|
|
203
|
+
fun Route.userRoutes() {
|
|
204
|
+
val userService by inject<UserService>()
|
|
205
|
+
// ...
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Anti-Patterns
|
|
210
|
+
|
|
211
|
+
```kotlin
|
|
212
|
+
// Never: blocking calls in coroutine context
|
|
213
|
+
suspend fun fetchData(): Data {
|
|
214
|
+
val response = okHttpClient.newCall(request).execute() // BLOCKS the thread
|
|
215
|
+
// Use: a suspend-compatible HTTP client (Ktor Client, suspend wrappers)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Never: exposing mutable state from services
|
|
219
|
+
class UserService {
|
|
220
|
+
val users = mutableListOf<User>() // Anyone can mutate
|
|
221
|
+
// Use: private mutableListOf, expose as List
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Never: Java-style builder patterns (use named arguments + copy())
|
|
225
|
+
User.builder().name("Alice").email("a@b.com").build()
|
|
226
|
+
// Use: User(name = "Alice", email = "a@b.com")
|
|
227
|
+
```
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# Kotlin Language Features
|
|
2
|
+
|
|
3
|
+
Modern Kotlin used deliberately. Every feature exists for safety, clarity, or expressiveness.
|
|
4
|
+
|
|
5
|
+
## Null Safety
|
|
6
|
+
|
|
7
|
+
```kotlin
|
|
8
|
+
// The type system enforces null safety — respect it
|
|
9
|
+
fun findUser(email: String): User? {
|
|
10
|
+
return userRepository.findByEmail(email)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Safe call chain
|
|
14
|
+
val cityName = user?.address?.city?.name
|
|
15
|
+
|
|
16
|
+
// Elvis operator for defaults
|
|
17
|
+
val displayName = user?.name ?: "Anonymous"
|
|
18
|
+
|
|
19
|
+
// Elvis with throw for required values
|
|
20
|
+
val userId = request.userId ?: throw IllegalArgumentException("userId is required")
|
|
21
|
+
|
|
22
|
+
// Smart casts — the compiler narrows for you
|
|
23
|
+
fun process(value: Any) {
|
|
24
|
+
if (value is String) {
|
|
25
|
+
println(value.length) // Smart cast to String
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Safe cast
|
|
30
|
+
val number = value as? Int // null if not Int, no ClassCastException
|
|
31
|
+
|
|
32
|
+
// NEVER: !! without proof
|
|
33
|
+
user!!.name // If user is null → NPE. Only use when you've verified externally.
|
|
34
|
+
// Prefer: requireNotNull(user) { "User must exist after authentication" }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Rules
|
|
38
|
+
|
|
39
|
+
- `!!` is a code smell. Every usage needs a comment explaining why it's safe
|
|
40
|
+
- Prefer `requireNotNull()` or `checkNotNull()` — they provide meaningful error messages
|
|
41
|
+
- Use `?.let { }` for nullable transformations
|
|
42
|
+
- Return nullable types from functions that legitimately might not find a result
|
|
43
|
+
|
|
44
|
+
## Data Classes
|
|
45
|
+
|
|
46
|
+
```kotlin
|
|
47
|
+
// Immutable data carriers — the default for DTOs, events, value objects
|
|
48
|
+
data class CreateUserRequest(
|
|
49
|
+
val name: String,
|
|
50
|
+
val email: String,
|
|
51
|
+
val role: UserRole = UserRole.USER
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
// Defensive copying for collections
|
|
55
|
+
data class Order(
|
|
56
|
+
val id: OrderId,
|
|
57
|
+
val customerId: CustomerId,
|
|
58
|
+
val items: List<OrderItem>, // Immutable List, not MutableList
|
|
59
|
+
val status: OrderStatus,
|
|
60
|
+
val createdAt: Instant
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Non-destructive updates
|
|
64
|
+
val updatedOrder = order.copy(status = OrderStatus.SHIPPED)
|
|
65
|
+
|
|
66
|
+
// Value objects with validation
|
|
67
|
+
@JvmInline
|
|
68
|
+
value class Email(val value: String) {
|
|
69
|
+
init {
|
|
70
|
+
require(value.contains("@")) { "Invalid email: $value" }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@JvmInline
|
|
75
|
+
value class UserId(val value: String) {
|
|
76
|
+
init {
|
|
77
|
+
require(value.isNotBlank()) { "UserId must not be blank" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Sealed Types
|
|
83
|
+
|
|
84
|
+
```kotlin
|
|
85
|
+
// Exhaustive state modeling — the compiler enforces completeness
|
|
86
|
+
sealed interface PaymentResult {
|
|
87
|
+
data class Success(val transactionId: String, val processedAt: Instant) : PaymentResult
|
|
88
|
+
data class Failure(val errorCode: String, val message: String) : PaymentResult
|
|
89
|
+
data class Pending(val referenceId: String, val estimatedWait: Duration) : PaymentResult
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Exhaustive when — add a new subtype, get a compile error everywhere
|
|
93
|
+
fun describe(result: PaymentResult): String = when (result) {
|
|
94
|
+
is PaymentResult.Success -> "Paid: ${result.transactionId}"
|
|
95
|
+
is PaymentResult.Failure -> "Failed: ${result.message}"
|
|
96
|
+
is PaymentResult.Pending -> "Pending: ${result.referenceId}"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Result modeling
|
|
100
|
+
sealed interface Result<out T> {
|
|
101
|
+
data class Success<T>(val value: T) : Result<T>
|
|
102
|
+
data class Failure(val error: AppError) : Result<Nothing>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
sealed interface AppError {
|
|
106
|
+
data class NotFound(val entity: String, val id: String) : AppError
|
|
107
|
+
data class Validation(val errors: Map<String, String>) : AppError
|
|
108
|
+
data class Conflict(val message: String) : AppError
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Extension Functions
|
|
113
|
+
|
|
114
|
+
```kotlin
|
|
115
|
+
// Add behavior to types without inheritance
|
|
116
|
+
fun String.toSlug(): String =
|
|
117
|
+
this.lowercase()
|
|
118
|
+
.replace(Regex("[^a-z0-9\\s-]"), "")
|
|
119
|
+
.replace(Regex("\\s+"), "-")
|
|
120
|
+
.trim('-')
|
|
121
|
+
|
|
122
|
+
// Scoped extensions for domain logic
|
|
123
|
+
fun List<Order>.totalRevenue(): BigDecimal =
|
|
124
|
+
this.filter { it.status == OrderStatus.COMPLETED }
|
|
125
|
+
.sumOf { it.total }
|
|
126
|
+
|
|
127
|
+
// Extension properties
|
|
128
|
+
val String.isValidEmail: Boolean
|
|
129
|
+
get() = this.matches(Regex("^[^@]+@[^@]+\\.[^@]+$"))
|
|
130
|
+
|
|
131
|
+
// Generic extensions
|
|
132
|
+
fun <T> T.also(block: (T) -> Unit): T {
|
|
133
|
+
block(this)
|
|
134
|
+
return this
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Scope Functions
|
|
139
|
+
|
|
140
|
+
```kotlin
|
|
141
|
+
// let — transform nullable, scoped operations
|
|
142
|
+
val length = name?.let { it.trim().length }
|
|
143
|
+
|
|
144
|
+
// apply — configure objects
|
|
145
|
+
val connection = HttpClient().apply {
|
|
146
|
+
timeout = Duration.ofSeconds(30)
|
|
147
|
+
retries = 3
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// run — execute block with receiver
|
|
151
|
+
val result = StringBuilder().run {
|
|
152
|
+
append("Hello")
|
|
153
|
+
append(" ")
|
|
154
|
+
append("World")
|
|
155
|
+
toString()
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// also — side effects without changing the value
|
|
159
|
+
val user = createUser(request).also { logger.info("Created user: ${it.id}") }
|
|
160
|
+
|
|
161
|
+
// with — operate on an object
|
|
162
|
+
val summary = with(report) {
|
|
163
|
+
"$title: $total items, $revenue revenue"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Rules:
|
|
167
|
+
// - let: nullable chains, scoped transformations
|
|
168
|
+
// - apply: object configuration
|
|
169
|
+
// - run: compute a result from a receiver
|
|
170
|
+
// - also: side effects (logging, validation)
|
|
171
|
+
// - with: multiple operations on same object (avoid for nullable)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Collection Operations
|
|
175
|
+
|
|
176
|
+
```kotlin
|
|
177
|
+
// Kotlin collections are immutable by default
|
|
178
|
+
val users: List<User> = fetchUsers() // Immutable
|
|
179
|
+
val mutableUsers: MutableList<User> = mutableListOf() // Explicit mutation
|
|
180
|
+
|
|
181
|
+
// Functional chains
|
|
182
|
+
val activeAdminEmails = users
|
|
183
|
+
.filter { it.isActive }
|
|
184
|
+
.filter { it.role == Role.ADMIN }
|
|
185
|
+
.map { it.email }
|
|
186
|
+
.sorted()
|
|
187
|
+
|
|
188
|
+
// groupBy, associateBy, partition
|
|
189
|
+
val (active, inactive) = users.partition { it.isActive }
|
|
190
|
+
val byDepartment = users.groupBy { it.department }
|
|
191
|
+
val byId = users.associateBy { it.id }
|
|
192
|
+
|
|
193
|
+
// Sequences for large collections (lazy evaluation)
|
|
194
|
+
val result = users.asSequence()
|
|
195
|
+
.filter { it.isActive }
|
|
196
|
+
.map { it.name }
|
|
197
|
+
.take(10)
|
|
198
|
+
.toList()
|
|
199
|
+
|
|
200
|
+
// buildList, buildMap, buildSet
|
|
201
|
+
val items = buildList {
|
|
202
|
+
add("first")
|
|
203
|
+
addAll(existingItems)
|
|
204
|
+
if (includeExtra) add("extra")
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Anti-Patterns
|
|
209
|
+
|
|
210
|
+
```kotlin
|
|
211
|
+
// Never: !! without justification
|
|
212
|
+
user!!.name // NPE waiting to happen
|
|
213
|
+
|
|
214
|
+
// Never: var when val works
|
|
215
|
+
var name = "Alice" // Will it change? If not, use val
|
|
216
|
+
|
|
217
|
+
// Never: MutableList in public APIs
|
|
218
|
+
fun getUsers(): MutableList<User> // Caller can mutate your internal state
|
|
219
|
+
// Use: fun getUsers(): List<User>
|
|
220
|
+
|
|
221
|
+
// Never: Java-style static utilities
|
|
222
|
+
object StringUtils { fun format(s: String): String = ... }
|
|
223
|
+
// Use: extension functions — fun String.format(): String = ...
|
|
224
|
+
|
|
225
|
+
// Never: when without exhaustive matching on sealed types
|
|
226
|
+
when (result) {
|
|
227
|
+
is Success -> handle(result)
|
|
228
|
+
else -> {} // Silently ignores new subtypes
|
|
229
|
+
}
|
|
230
|
+
// Remove else — let the compiler catch missing cases
|
|
231
|
+
```
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Kotlin Expert Overview
|
|
2
|
+
|
|
3
|
+
Principal-level Kotlin engineering. Deep language mastery, coroutines, multiplatform, and idiomatic patterns.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This guide applies to:
|
|
8
|
+
- Backend services (Ktor, Spring Boot, Quarkus)
|
|
9
|
+
- Android applications (Jetpack Compose, Architecture Components)
|
|
10
|
+
- Kotlin Multiplatform (KMP) — shared code across Android, iOS, web, desktop
|
|
11
|
+
- CLI tools and scripting
|
|
12
|
+
- Libraries and Maven/Gradle artifacts
|
|
13
|
+
- Data processing and streaming
|
|
14
|
+
|
|
15
|
+
## Core Philosophy
|
|
16
|
+
|
|
17
|
+
Kotlin is a pragmatic language. It gives you safety, expressiveness, and interoperability — use all three.
|
|
18
|
+
|
|
19
|
+
- **Null safety is the foundation.** The type system distinguishes nullable from non-nullable. Respect it — no `!!` without proof.
|
|
20
|
+
- **Immutability by default.** `val` over `var`, immutable collections, data classes. Mutation is explicit and intentional.
|
|
21
|
+
- **Conciseness without cleverness.** Kotlin lets you write less code. That doesn't mean you should write unreadable code.
|
|
22
|
+
- **Coroutines are structured.** Structured concurrency is not optional — every coroutine has a scope, every scope has a lifecycle.
|
|
23
|
+
- **Interop is a feature, not a compromise.** Kotlin's Java interop is seamless. Use Java libraries freely, but write Kotlin idiomatically.
|
|
24
|
+
- **If you don't know, say so.** Admitting uncertainty is professional. Guessing at coroutine behavior you haven't verified is not.
|
|
25
|
+
|
|
26
|
+
## Key Principles
|
|
27
|
+
|
|
28
|
+
1. **Null Safety Is Non-Negotiable** — No `!!` without documented justification. Use safe calls, elvis, and smart casts
|
|
29
|
+
2. **Immutability by Default** — `val`, `List` (not `MutableList`), `data class`, `copy()`
|
|
30
|
+
3. **Structured Concurrency** — Every coroutine lives in a scope. No `GlobalScope`. No fire-and-forget
|
|
31
|
+
4. **Extension Functions Over Utility Classes** — Extend types at the call site, not in static helpers
|
|
32
|
+
5. **Sealed Types for State Modeling** — Exhaustive `when` expressions, impossible states are compile errors
|
|
33
|
+
|
|
34
|
+
## Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
project/
|
|
38
|
+
├── src/main/kotlin/com/example/myapp/
|
|
39
|
+
│ ├── Application.kt # Entry point
|
|
40
|
+
│ ├── config/ # Configuration
|
|
41
|
+
│ ├── domain/ # Core domain (no framework deps)
|
|
42
|
+
│ │ ├── model/ # Data classes, value objects, sealed types
|
|
43
|
+
│ │ ├── service/ # Domain services
|
|
44
|
+
│ │ └── event/ # Domain events
|
|
45
|
+
│ ├── application/ # Use cases, orchestration
|
|
46
|
+
│ │ ├── command/
|
|
47
|
+
│ │ ├── query/
|
|
48
|
+
│ │ └── port/ # Interfaces
|
|
49
|
+
│ ├── infrastructure/ # External concerns
|
|
50
|
+
│ │ ├── persistence/ # Database (Exposed, JPA, R2DBC)
|
|
51
|
+
│ │ ├── messaging/ # Kafka, RabbitMQ
|
|
52
|
+
│ │ └── client/ # HTTP clients
|
|
53
|
+
│ └── api/ # REST/gRPC endpoints
|
|
54
|
+
│ ├── route/ # Ktor routes or Spring controllers
|
|
55
|
+
│ └── dto/ # Request/response models
|
|
56
|
+
├── src/test/kotlin/com/example/myapp/
|
|
57
|
+
│ ├── unit/
|
|
58
|
+
│ ├── integration/
|
|
59
|
+
│ └── architecture/
|
|
60
|
+
├── build.gradle.kts
|
|
61
|
+
└── Dockerfile
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Definition of Done
|
|
65
|
+
|
|
66
|
+
A Kotlin feature is complete when:
|
|
67
|
+
|
|
68
|
+
- [ ] Compiles with zero warnings (`-Werror` or `allWarningsAsErrors = true`)
|
|
69
|
+
- [ ] All tests pass
|
|
70
|
+
- [ ] No `!!` without documented justification
|
|
71
|
+
- [ ] No `var` where `val` suffices
|
|
72
|
+
- [ ] No `MutableList`/`MutableMap` exposed in public APIs
|
|
73
|
+
- [ ] Coroutines use structured concurrency (no `GlobalScope`)
|
|
74
|
+
- [ ] Nullable types handled explicitly (no suppressed warnings)
|
|
75
|
+
- [ ] Detekt reports zero findings
|
|
76
|
+
- [ ] No `TODO` without an associated issue
|
|
77
|
+
- [ ] Code reviewed and approved
|