DEV Community

Cover image for I made every Java challenge in my app a public link you can solve without signing up. Roast the setup?
Nextpatch
Nextpatch

Posted on

I made every Java challenge in my app a public link you can solve without signing up. Roast the setup?

A few weeks ago I posted about
NextPatch,
a small daily habit for Java developers: a 45-second summary of one change between
versions, then a 2-minute challenge.

Thanks for the feedback on that one. Re-reading the post afterwards, one thing bothered
me: every link ended on a login screen. I was asking people to create an account
before they'd seen a single challenge. I wouldn't do that for a quiz app either.

So I spent the last few weeks on one idea:

Every challenge should be a link you can share, open on your phone and solve in
under 30 seconds, without an account.

Here's how I built it. I'd really like your opinion on the design, and on how it
feels if you try one.

-> Today's challenge: nextpatch.dev/reto-de-hoy
-> All of them: nextpatch.dev/retos

1. Every challenge gets a URL that never changes

Challenges don't have titles, so the slug is built from the lesson title plus the
challenge's position in it:

/reto/var-adios-a-repetir-el-tipo-2
Enter fullscreen mode Exit fullscreen mode

On a collision it appends -2, -3... The important part is that the slug is set
when the challenge is created and never recalculated
, even if someone renames the
lesson or reorders it. A link someone shared on WhatsApp six months ago still has to
work.

Production already had ~560 challenges, so a startup runner fills in the missing slugs.
It's idempotent: once every challenge has one, it does nothing.

(Yes, the URLs are in Spanish. That's where the project started. Is it weird for you
as an English speaker? Honest question.)

2. Grading without an account, and without leaking the answer

The page calls two anonymous endpoints:

GET  /api/v1/public/challenges/{slug}        → the challenge, no solution
POST /api/v1/public/challenges/{slug}/check  → { answer } → { correct, explanation }
Enter fullscreen mode Exit fullscreen mode

Each challenge stores a payload (what you see) and a solution (what the server
checks), and the solution is never serialized. Grading is deterministic: the server
compares option ids, tokens or line orders. No user code is ever executed. No
sandbox, no containers, so it all runs fine on a small home server.

The anonymous check doesn't save anything and doesn't give XP. It just tells you
whether you were right and why.

3. Solve first, sign up later, and keep the credit

This is the part I'm least sure about. I'd love your take.

If you get it right without being logged in, the server sets a cookie:

np_reto = slug.epochSeconds.HMAC-SHA256
Enter fullscreen mode Exit fullscreen mode

It's signed so you can't forge "I solved 50 challenges", it's HttpOnly and Secure,
and it expires after 24 hours. Only after you get it right does the page show "Keep
your streak: create an account". It never asks before.

If you sign up or log in while that cookie is alive, the challenge is claimed: it
counts as solved, you get its XP, and today counts as day 1 of your streak. Claiming is
idempotent: if you'd already solved it, nothing happens. And it doesn't unlock the
lesson. You still walk the path in order.

Why a cookie and not a row in the database? The server doesn't create any record for
anonymous visitors. Your progress only exists on the server once you decide to sign up.

4. A "challenge of the day" that's the same for everyone

/reto-de-hoy redirects to one challenge, the same for everyone that day. It's picked
deterministically from the date:

static String choose(LocalDate day, List<Candidate> candidates) {
    var easy = candidates.stream()
            .filter(c -> c.difficulty() <= 2 && QUICK_TYPES.contains(c.type()))
            .toList();
    var pool = easy.isEmpty() ? candidates : easy;
    return pool.get(Math.floorMod(scramble(day.toEpochDay()), pool.size())).slug();
}
Enter fullscreen mode Exit fullscreen mode

Two details:

  • It only picks easy, quick types (multiple choice, fill in the blank, predict the output). The daily challenge is a first impression, not a boss fight.
  • scramble is the SplitMix64 finalizer. With the raw day number, consecutive days would land on neighbouring challenges from the same lesson. Mixing the bits spreads them across the whole catalog.

The choice is cached in memory until the date changes. Publishing new content
mid-morning doesn't change today's challenge for someone who has already seen it.

5. Link previews drawn with Java2D

A shared link is only as good as its preview. Every challenge has an Open Graph image
(1200×630): the code with syntax highlighting and the blank drawn as an empty box.

I didn't want a headless browser on a small server, so it's plain Java2D with the
fonts bundled in the jar. Two decisions I like:

  • The renderer only gets the same DTO as the public page, so the answer can't end up in the image.
  • The image URL carries a hash of the challenge's content. If the challenge changes, the URL changes, and LinkedIn, WhatsApp and the CDN fetch the new image. If it doesn't, the image is drawn once and served from cache.

6. The test I set before promoting anything

I set myself one rule before posting anywhere: someone who opens a shared link on their
phone, with no account, solves a challenge in under 30 seconds.
The first time I tried,
on mobile, the challenge started about 1,230 px down the page, below the theory.
Now it's right under the title and the explanation is folded behind "Why? See the
explanation". My last run took 3.6 seconds from tapping the link to "Correct!".

That was me, though, and I know where everything is. That's why I'm writing this.

What I'd love from you

On the design:

  • Solve-first, sign-up-later with a signed cookie: would you do it differently?
  • Slugs that never change, even when the title does: right call, or will it bite me?
  • Java2D for link previews instead of a headless browser: clever or cursed?

On the experience (if you have 2 minutes):

  1. Open today's challenge on your phone.
  2. Solve it, or fail it. Both are useful to me.
  3. Tell me in the comments: how long did it take, what slowed you down, and did the explanation help?

Anything that felt slow, confusing or ugly is exactly what I want to hear.
Thanks for reading :)

Top comments (0)