Featured resource
2026 Tech Forecast
2026 Tech Forecast

1,500+ tech insiders, business leaders, and Pluralsight Authors share their predictions on what’s shifting fastest and how to stay ahead.

Download the forecast
  • Lab
    • Libraries: If you want this lab, consider one of these libraries.
    • Core Tech
Labs

Build an Event Scheduler in Kotlin

In this lab, you'll build a working event scheduler in Kotlin, applying data modeling, input validation, and error handling to create a reliable terminal application. You'll persist events to a local file and implement reminder logic that surfaces upcoming events each time the app runs. By the time you're done, you'll have hands-on experience with the patterns that make Kotlin programs both robust and maintainable.

Lab platform
Lab Info
Level
Intermediate
Last updated
Jul 14, 2026
Duration
45m

Contact sales

By clicking submit, you agree to our Privacy Policy and Terms of Use, and consent to receive marketing emails from Pluralsight.
Table of Contents
  1. Challenge

    Step 1: Model events with data classes

    Introduction

    Event schedulers are a practical exercise in the skills Kotlin developers use every day: modeling data cleanly, handling errors gracefully, and keeping the application honest through tests.

    In this lab you'll build a console-based scheduler your team could actually use from the terminal, persisting events to a local file and surfacing upcoming reminders automatically each time the app starts.

    How this lab works

    The lab takes a test-driven approach. Tests for every task are pre-written, but only the first one is visible at the start. Completing a task's check automatically unlocks the next task's test. Each task follows three steps:

    1. Specify: Read the test that's now visible to understand what the code must do, then run it to see it fail.
    2. Implement: Write the code that makes the test pass.
    3. Confirm: Rerun the tests to verify. Passing the task's check unlocks the next test automatically.

    Explore the starter project

    To get the most out of this structure, read each test before writing any implementation code.

    info> Feeling stuck? Check out the matching solution/stepN/ folder for the step you're on to see a working implementation. Give it a try on your own first. The solution folder is your safety net, not your starting point.

    The application directory already contains the Gradle project with source files in src/main/kotlin/com/scheduler/ and pre-written tests in src/test/kotlin/com/scheduler/.

    testCoreProperties (Task 1.1) is visible from the start; every other test unlocks as you complete the task before it.

    If you ever want to check which tasks haven't unlocked yet, or reveal one manually, run the following command from the application folder:

    ./reveal.sh
    

    Kotlin data classes

    A data class in Kotlin automatically generates equals(), hashCode(), toString(), and copy() from the properties in its primary constructor. This makes data classes the right choice for representing structured, comparable values such as events.

    Declare properties with val (read-only) or var (mutable) directly in the constructor parameter list.

    data class Item(val id: String, val name: String, val count: Int)
    

    LocalDateTime from java.time represents a date and time without a timezone offset and works as a constructor argument like any other type.

    Before you begin

    warning> The lab environment on the right may take up to a minute to finish loading. If you don't see the terminal or project files yet, please wait a moment before continuing. ### Typed categories with enums

    An enum class defines a fixed set of named constants, making it a better choice than a plain String for a bounded list of values such as event categories.

    Enum values are referenced by name through the enum type, and valueOf() converts a string to the matching constant at runtime. Prefer an enum over a string here because it prevents typos and makes exhaustive pattern matching possible later.

    enum class Kind { ALPHA, BETA, GAMMA }
    
    val k = Kind.valueOf("ALPHA") // Kind.ALPHA
    ``` info> This lab experience was developed by the Pluralsight team using an internally developed AI tool. All sections were verified by human experts for accuracy prior to publication. However, content may still contain errors or inaccuracies, and we recommend independent verification.
    <br></br> To report a problem or provide feedback, [click here](https://help.pluralsight.com/hc/en-us/requests/new). Feedback may be used to improve accuracy in accordance with our Privacy Policy.
    
  2. Challenge

    Step 2: Build the event management menu

    With Event and EventCategory defined and tested, you have a typed model to build on.

    In this step you'll implement the four CRUD functions in EventManager.kt and wire them to the main menu, guided by four tests in EventManagerTest.kt.

    Each test simulates keyboard input with System.setIn and captures standard output to verify what the functions print. Each test unlocks automatically once the task before it passes its check.

    Read it before writing anything, and let the failure message guide your implementation. By the end of this step, all four operations are reachable from the menu.

    Reading input and constructing an event

    readLine() reads one line from standard input and returns String? (nullable).

    Print a short prompt with println() before each call so the person running the app knows what to type, then assign the result to a variable and use the Elvis operator (?:) to return early on null input.

    After reading all fields, construct an Event by passing them to the constructor. Use UUID.randomUUID().toString() to generate a unique string ID for each event.

    println("Field:")
    val field = readLine() ?: return
    val item = Item(id = UUID.randomUUID().toString(), name = field)
    items.add(item)
    ``` ### Listing items and handling an empty collection
    
    Before iterating a list to print its contents, guard against the empty case with `isEmpty()` and return early with a message. 
    
    The remaining code can then assume the list has items and use `forEach` to print each one without an `else` branch.
    
    ```kotlin
    if (items.isEmpty()) {
        println("No items found.")
        return
    }
    items.forEach { println(it) }
    ``` ### Updating a record with copy()
    
    Data classes provide a `copy()` function that creates a new instance with specified fields replaced and the rest unchanged. 
    
    Locate the event to update with `find`, call `copy()` with only the fields that should change, then replace the original in the list at the same index. This avoids mutating the original object and makes the intent of the update explicit.
    
    ```kotlin
    val item = items.find { it.id == targetId } ?: return
    val index = items.indexOf(item)
    
    items[index] = item.copy(name = newName)
    ``` ### Removing a list item and routing menu choices
    
    `MutableList.remove()` removes the first element equal to its argument. Combined with `find`, this lets you locate an event by ID and remove it without needing its index. 
    
    After adding `deleteEvent()`, complete the `when` block in `Main.kt` so each menu number routes to the corresponding function.
  3. Challenge

    Step 3: Add input validation and error handling

    All four CRUD functions pass their tests, but those tests only cover the happy path.

    In this step, you'll unlock three more tests, one task at a time, that specify what the scheduler must do when input goes wrong:

    • A date string in the wrong format
    • An ID that matches no event
    • A non-numeric menu choice

    Read each test carefully before implementing. The failure messages describe exactly where the guard goes and what it must do when it fires.

    Catching exceptions from invalid input

    LocalDateTime.parse() throws DateTimeParseException when the input string does not match the expected format.

    Wrap the call in a try/catch block and handle the exception by printing a descriptive error message and returning early so the operation leaves the list unchanged.

    try {
        val parsed = SomeType.parse(rawInput)
        // use parsed value
    } catch (e: SomeParseException) {
        println("Invalid input format.")
        return
    }
    ``` ### Guarding against missing records
    
    When `find` returns `null`, no event matched the requested ID. An explicit `if (event == null)` check lets you print a meaningful error message and return before the function attempts to operate on a missing record.
    
    This is preferable here to a silent `?: return` because printing the message is part of the expected behavior.
    
    ```kotlin
    val item = items.find { it.id == targetId }
    
    if (item == null) {
        println("Item not found.")
        return
    }
    // use item
    ``` ### Handling invalid menu input
    
    `toInt()` throws `NumberFormatException` when its receiver string is not a valid integer. Wrapping the conversion in a `try/catch` handles letters, symbols, and empty input. 
    
    A separate `else` branch in the `when` block covers integers that parse correctly but fall outside the expected range. Both cases should print an informative error and loop back to the menu prompt.
  4. Challenge

    Step 4: Persist events and surface reminders

    With a validated CRUD interface passing all its tests, the scheduler accepts commands but forgets every event when it exits.

    In this step you'll unlock three more tests, one task at a time, that specify file persistence and startup reminder behavior, connecting the in-memory events list to a local file and surfacing any upcoming events each time the application starts.

    Writing event data to a file

    File("path").writeText(content) writes a string to disk, creating or replacing the file.

    To serialize a list of events into a single string, use joinToString(separator = "\n") with a transform lambda that formats each event as a pipe-delimited line. The resulting multi-line string can be written to the file in one call.

    val content = items.joinToString("\n") { "${it.field1}|${it.field2}" }
    File("output.txt").writeText(content)
    ``` ### Reading and reconstructing events from a file
    
    Check whether `events.txt` exists with `File.exists()` before reading it. If it does not exist, return an empty mutable list.
    
    If the file exists:
    
    - Use `readLines()` to read each line as a `String`.
    - Filter out blank lines with `filter { it.isNotBlank() }`.
    - Use `split("|")` to separate the five event fields.
    - Parse the date/time field with `LocalDateTime.parse()`.
    - Convert the category field with `EventCategory.valueOf()`.
    - Reconstruct and return each `Event`.
    
    
    <details>
    <summary>Hint: how to structure the line-parsing logic</summary>
    
    ```kotlin
    val parts = line.split("|")
    val item = Item(
        id = parts[0],
        name = parts[1],
        kind = Kind.valueOf(parts[2])
    )
    
    ### Filtering events by time

    LocalDateTime.now() returns the current date and time. Add 24 hours to it with plusHours(24) to compute the cutoff for upcoming events.

    Use filter on the events list with a predicate that checks whether each event's dateTime falls between now and the cutoff, then print a reminder line for each match that includes the event's title and date/time. ### Running the full application

    All tasks are now implemented and validated. Running the application from the command line lets you experience the complete system.

    Events created in one session are loaded from the file in the next, and any events scheduled within 24 hours appear as reminders before the menu. Well done! You built a working event scheduler in Kotlin.

    You:

    • Modeled event data with a Kotlin data class and a typed enum
    • Wired a menu-driven CLI that supports creating, listing, updating, and deleting events
    • Used copy() to update records without mutation
    • Guarded against malformed input with try/catch blocks and explicit null checks
    • Connected file persistence and startup reminder logic so the scheduler retains events across sessions

    The patterns you applied here transfer directly to any Kotlin application that reads, modifies, and reliably stores structured data.

    Typed modeling, defensive input handling, and file-based persistence are foundational skills in Kotlin back-end and mobile development alike.

About the author

Pluralsight’s AI authoring technology is designed to accelerate the creation of hands-on, technical learning experiences. Serving as a first-pass content generator, it produces structured lab drafts aligned to learning objectives defined by Pluralsight’s Curriculum team. Each lab is then enhanced by our Content team, who configure the environments, refine instructions, and conduct rigorous technical and quality reviews. The result is a collaboration between artificial intelligence and human expertise, where AI supports scale and efficiency, and Pluralsight experts ensure accuracy, relevance, and instructional quality, helping learners build practical skills with confidence.

Real skill practice before real-world application

Hands-on Labs are real environments created by industry experts to help you learn. These environments help you gain knowledge and experience, practice without compromising your system, test without risk, destroy without fear, and let you learn from your mistakes. Hands-on Labs: practice your skills before delivering in the real world.

Learn by doing

Engage hands-on with the tools and technologies you’re learning. You pick the skill, we provide the credentials and environment.

Follow your guide

All labs have detailed instructions and objectives, guiding you through the learning process and ensuring you understand every step.

Turn time into mastery

On average, you retain 75% more of your learning if you take time to practice. Hands-on labs set you up for success to make those skills stick.

Get started with Pluralsight