Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
54: Annotations in Kotlin
At some point in your career, you're going to run into a problem where you need to attach extra information to a piece of code—not information that changes how the logic executes in the moment, but information that something else (like a framework or a validator) can use to decide how to treat that code. This is exactly what annotations are for.
The manual checklist headache
Imagine we're building a system to validate user profiles. We have a User data class, and we need to ensure that the username isn't empty and the email looks like an actual email. The naive way to handle this is to write a dedicated validation function that manually checks every field.
data class User(val username: String, val email: String, val bio: String)
fun validateUser(user: User) {
if (user.username.isEmpty()) throw Exception("Username cannot be empty")
if (!user.email.contains("@")) throw Exception("Invalid email")
// bio can be empty, so we leave it alone
}
This works fine when you have three fields. But as the project grows, you'll end up with fifty fields across twenty different data classes. You'll find yourself writing the same if (field.isEmpty()) logic over and over again. It's repetitive, and it's incredibly easy to add a new field to a class and simply forget to update the validation function. I've seen entire production outages caused by someone adding a "Required" field to a database model but forgetting to add the corresponding if statement in the validation layer.
Tagging your data for later
The better way is to stop treating validation as a sequence of hardcoded checks and start treating it as a set of rules attached to the data itself. We can do this by defining our own annotations. Instead of a manual checklist, we "tag" the properties we care about.
First, we need to tell Kotlin where these annotations can actually be used and how long they should stick around. If you don't specify the @Retention, the annotation is discarded after compilation, which means you can't see it at runtime via reflection. For our validator, we need RUNTIME retention.
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class NotEmpty
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.RUNTIME)
annotation class EmailFormat
Now, our User class becomes declarative. We aren't writing logic; we're describing the state we expect:
data class User(
@NotEmpty val username: String,
@EmailFormat val email: String,
val bio: String
)
The trade-off: Reflection vs. Boilerplate
Now you might be wondering: "Wait, I still have to write the code that actually checks the annotations, right?" Yes, you do. But here is the win: you only write that logic once for the entire application.
By using reflection, we can write a single validator that looks at any object, finds the fields marked with @NotEmpty, and checks if they are blank. I'll be honest with you—reflection is slower than a direct if statement. In a tight loop running millions of times a second, this would be a mistake. But for a user registration form? The performance hit is nanoseconds, while the developer productivity gain is massive. You've traded a tiny bit of CPU time for a system where adding a new validation rule is as simple as adding one word above a property.
The real magic happens when you realize this is how almost every modern JVM framework operates. Spring uses @Autowired, JUnit uses @Test, and Hibernate uses @Entity. They aren't magic; they're just scanning your code for these tags and executing a predefined behavior based on what they find.
📋 Practical Task
Build a Permission-Based Method Guard
You are building a security module where certain functions should only be executed if the user has a specific permission level. Instead of wrapping every function body in an if (user.hasPermission(...)) block, you will implement an annotation-based guard.
Your Task:
- Create an annotation called
@RequiresPermissionthat accepts a String parameter (the name of the required permission). - Ensure the annotation is targeted at
AnnotationTarget.FUNCTIONand hasAnnotationRetention.RUNTIME. - Create a
SecurityManagerclass with a function calledexecuteSecurely(func: KFunction<*>, user: User). - Inside
executeSecurely, use reflection to check if the function is annotated with@RequiresPermission. - If the annotation exists, check if the
userobject possesses the required permission string. If they don't, throw aSecurityException. If they do (or if there is no annotation), call the function.
Hint: You will need to import kotlin.reflect.full.* to access the findAnnotation extension function.
There are no comments for now.