
The power of exercises: How to design them well
When authors design exercises for books or courses, they typically try to shape it in the way it practices as many concepts from the chapter/lesson as possible. This is of course an important priority when designing an exercise, but I believe it shouldn't be the most important. Patterns expected to use in exercises have an exceptionally high chance to stick with the learner. Before giving exercise to a group, look at the expected solution and ask yourself "Do I want to see this all around my codebase?". If the answer is "No", redesign the exercise.
Coroutines Mastery 2025, the biggest course teaching kotlinx.coroutines has just completed. Attendees were faced with 34 exercises. Then they had to implement a final project, which was a community contribution of their choice. Reviewing those final projects, I could see again and again the patterns expected in exercises. Sometimes used properly, sometimes misused, but very often present in all kinds of projects: sample applications, libraries, articles.
The course equipped attendees with some tools, and they started using them. What surprised me is the disproportion. I presented many practical patterns in lessons, with deep explanations and practical examples, however, when I looked at the final projects, I could see clear overrepresentation of those patterns that were practices in exercises. It makes sense: exercises are more engaging, they require active participation, they require thinking, and they require solving problems. Lessons are more passive, they can be skimmed through, or even skipped.
It is great I designed my exercises to practice common patterns that are best practices. The only exception was exercise after the lesson about Channels. Channels require a separate lesson, as they are important and used by many other elements. However, there are not many common patterns that require channels. After thinking about it for some time, I found one, that is not very common, but at least can be found in some projects. It is to use a channel to synchronize operations:
class UserRefresher( private val scope: CoroutineScope, private val refreshData: suspend (Int) -> Unit, ) { private val queue = Channel<Int>(Channel.UNLIMITED) init { scope.launch { for (userId in queue) { refreshData(userId) } } } suspend fun refresh(userId: Int) { queue.send(userId) } }
This pattern makes sense in specific cases, however after reviewing another final project that uses it for no good reason, I decided this exercise is out. I need to find something else to practice Channels.
This leads to the first conclusion: Exercises should be above all designed patterns we want to see in our codabases. It is better to leave a lesson unpracticed than to practice a minor pattern that has better alternatives. There is also another lesson I learnt. Exercises should not make learners too eager to use some techniques. It's best if they teach not only when a pattern should be used, but also when a pattern should not be used. I am very proud of three exercises I designed for Coroutines Mastery, where learners needed to correct implemented classes that were not supporting cancellation. People needed to use yield, ensureActive, rethrow CancellationException, use withContext(NonCancellable). I had lots of work reviewing it, because I intentionally included there not only places where those structures should be added, but also places that seem to ask for them, but actually didn't need them at all. In some solutions, I needed to request changes three of four times with a comment, before learner finally removed all unnecessary structures, and kept only those that are actually useful.
Designing good exercises is hard, but I believe it is one of the most important elements of a good online course. Reviewing exercises and giving feedback seems exceptionally useful for learners. I am so happy we created a platform for that. Good exercise should:
- Show/Emulate a practical use case, and idiomatic solution to the problem.
- Practice as many skills/concepts covered in the lesson.
- Be as simple as possible and not require work on what is not related to the course.
- Give space not only for applying a structure, but also for an incorrect misuse, that should be pointed out by unit tests or review.
I hope this helps you design better exercises for your courses or books!