> Koala Academy
A structured onboarding programme for junior engineers. You'll build a todo application from a blank repository to a live, cloud-hosted service — learning the Koala way as you go.
How this works
You will build a single product — a todo list — from nothing to production. Each module adds a capability. The product is deliberately boring so you can focus on the how, not the what.
We won't tell you exactly which libraries to use, which patterns to adopt, or how to wire things together. Part of the exercise is working that out, defending it in code review, and rewriting when your first attempt turns out to be wrong. That's the job.
You are expected to use Claude Code heavily throughout. It's how we work day-to-day. Learn to drive it well: scope tasks tightly, read every diff it produces, push back when it's wrong. If you can't explain code in your own PR, the tool is using you — not the other way around.
Every change you make goes through the same pull request review process as production code. Expect feedback. Expect to be challenged. Expect to rewrite things you were proud of. Read CLAUDE.md in the main Koala repository — those conventions are binding from day one.
The programme runs for roughly three months. You'll also be embedded in a delivery team from day one — attending ceremonies, pairing on real work, and shipping small changes as you go.
What you'll learn
Technical
Non-technical
The modules
Each module is a deliberate step forward. Don't skip ahead. You are expected to pause at the end of each module for a review with your mentor before moving on.
Version control
git · githubGet a repository set up the way we work. Everything that follows lives in this repo and moves through pull requests. There are no exceptions, even when you're the only contributor.
- A private repository named <yourname>-todo in the Koala GitHub organisation.
- A protected main branch that can only be updated via reviewed pull requests.
- A README that a new joiner could read and be productive from.
Command line application
dotnet · c#A small CLI that lets someone list, add, complete, and remove todos. The product is trivial on purpose — the point is to land your first .NET project, your first tests, and your first PR against yourself.
- Todos persist for the lifetime of the process. Don't reach for a database yet.
- Unit tests cover every meaningful property and state transition.
- Invalid actions are handled as outcomes, not thrown exceptions.
Continuous integration
github actionsAutomate the checks that you'd otherwise forget to run. A pull request that can't be proven safe isn't a pull request — it's a guess.
- Every commit on every branch is built and tested on Linux, macOS, and Windows.
- Pull requests can only merge when the pipeline is green.
- Dependency updates happen automatically — your job is to keep the pipeline fast enough that merging them is cheap.
Razor Pages web application
razor · tailwindAdd a web application alongside the CLI. The CLI keeps working — both applications talk to the same domain. For now, every interaction on the web is a full page load: no JavaScript, no AJAX. If the page works with JavaScript disabled, you've done it right.
- Domain code is shared between the CLI and the web app. You do not copy-paste the todo type.
- Styling is done with utility classes, not bespoke CSS files per page.
- The design works at 320px wide. If it only works on a desktop, start again.
- All routes are lowercase. The UI is sentence case. These are not suggestions.
Observability
logging · tracingYou cannot fix what you cannot see. Your application must emit structured logs that a human can query when something goes wrong. A log like "Something broke" is worse than no log at all.
- Logs are structured, not strings. Fields like trace ID, user, and endpoint are queryable.
- Noise is suppressed. Swagger requests, static files, and health checks should not fill the log.
- You can answer, from the logs alone: "how many requests failed in the last hour, and why?"
Domain model
ddd · result patternYour todo type should stop being a property bag with public setters. Model every state transition as a method on the entity itself. Errors should be values your caller can handle, not exceptions it has to catch.
- Entities are constructed through factories. Construction either succeeds with a valid object or returns a failure.
- State changes are expressed as verbs, not assignments.
- Page handlers stay thin — they translate outcomes, they don't contain the rules.
Persistence
postgresql · ef coreTodos now outlive the process. Store them in PostgreSQL — it's what we run in production, so you might as well learn its quirks now. Write migrations as if someone else is running them against a production table tomorrow, because one day, someone will be.
- Both the CLI and the web app read and write against the same database.
- Schema changes are backwards-compatible with the previous version of the code.
- Integration tests run against a real database, not a mock.
- Soft delete, audit timestamps, and optimistic concurrency exist because real products need them.
Local development
taskfile · dockerLocal dependencies run in Docker. Common workflows are scripted with Taskfile. A new joiner should be able to clone, run one command, and have everything running locally — application, database, and anything else it talks to.
- Local services are disposable. They can be wiped and recreated at any time.
- If a command takes more than one line to remember, it's a task.
- CI uses the same tasks you run locally, so the two can't drift.
Authentication and authorization
kinde · policiesUsers sign in through Kinde — our hosted auth provider. You don't roll your own login screen, you don't store passwords, you don't reinvent OIDC. What you do own is authorization: making sure every user only ever sees their own todos. This is the single most common place juniors ship a serious bug. Assume a malicious user is hand-crafting URLs.
- Every database query is scoped to the authenticated user. No exceptions.
- A valid todo ID belonging to someone else returns "not found". It does not return 403. It does not return the todo.
- You have a test that proves user A cannot see, complete, or remove user B's todos.
- Secrets (Kinde client ID, client secret) are not committed to the repo.
Tag helpers and validation
shared components · fluentvalidationKoala has a shared component library expressed as tag helpers. Apply them. Consistency matters more than your personal taste in markup. Validation is server-side — client-side validation is not a thing we do.
- Buttons, cards, badges, tables, and icons come from the shared library, not hand-rolled markup.
- Validation rules live in exactly one place. When they fail, the user sees a helpful message next to the field, not at the top of the page.
Interactivity with Alpine.js
client-side · progressive enhancementNow you can sprinkle in client-side interactivity — dark mode, dropdowns, confirmation dialogs. The rule: if JavaScript fails to load, every feature must still work. No exceptions.
- JavaScript enhances. It does not enable.
- State lives in the DOM, not in a framework-specific store.
- A destructive action (delete, cancel) always asks for confirmation.
Partial page updates with Alpine-AJAX
ajax · partialsSwap fragments of the page instead of reloading the whole thing. Validate fields as the user tabs through the form. Open side panels that load their content on demand. Every one of these features must still have a full-page fallback route.
- Ctrl/Cmd+Click on any enhanced link opens the full-page version in a new tab.
- Forms post over AJAX when JavaScript is available, and as traditional forms when it isn't.
- Nothing about this should feel like a single-page app. You are enhancing HTML, not replacing it.
Caching
redis · hybrid cacheMake hot reads fast without making them wrong. A cache that returns stale data after a write is a bug factory.
- Cache keys are scoped so one user's invalidation doesn't affect another.
- Every mutation that affects a cached value invalidates that value.
- There is a test that would fail if someone broke invalidation.
Cloud hosting
azure · cloudflareShip the application to Azure, on a subdomain somebody external could actually hit. This is the first time the thing you've built exists outside your laptop. It's a bigger deal than it sounds.
- The application, the database, and the cache all run as managed services.
- Logs from production are queryable by the same tool you use locally.
- The site serves over HTTPS on a koala subdomain.
Continuous deployment
bicep · github actionsA merge to main should deploy automatically, safely, and without anyone watching. Infrastructure is described in code — you should be able to reproduce the entire environment from the repository.
- Infrastructure changes are backwards-compatible with the currently running code.
- Database migrations are applied before traffic is shifted to the new version.
- A failed deployment leaves the previous version serving users.
House rules
None of these are negotiable. They exist because we've seen the alternative.
Consistency beats cleverness
Before you write something new, look at how the rest of the codebase does it. Match. If you think there's a better way, open a conversation — don't just ship it.
Small, reviewable pull requests
A PR that can't be reviewed in thirty minutes is too big. Split it. The review is where learning happens — don't deny yourself that by shipping mountains.
Prove it works
A module isn't done because you think it's done. It's done when the tests pass, the pipeline is green, and you've shown somebody the feature working end-to-end.
Document as you go
Keep notes on what you tried, what you chose, and why. When we review your work, we're as interested in your reasoning as in your code.
Ask early, ask specifically
Stuck for more than an hour? Ask. Come with what you've tried and what you expected to happen. We'd rather unblock you than watch you suffer in silence.