Normally you can just pass id as the x values, but then the boxes will be in the correct numerical positions.
In this case here, the best is probably just to replace 3, 17 and 21 with 1, 2 and 3 change the xticks to show 3, 17 and 21 - or to pad the values (they are sorted alphabetically)

Thanks. I figured out a similar solution today (by replacing original id values with properly lexicographically ordered ones) but it seemed a little ugly and introduced unnecessary noise.

It is much better if there is some option like order to specify the order directly.

For this type of problem, @sprintf is our best friend:

using Plots, StatsPlots, DataFrames, Printf
df = DataFrame(
id=repeat([3, 17, 21], inner=10),
value=randn(30)
)
str = [ @sprintf("%02i", x) for x in df.id ]
@df df boxplot(str, :value, fillalpha=0.75, linewidth=1)
# or to keep similar syntax, one may define a sprintf02i() function:
sprintf02i(vecx) = [ @sprintf("%02i", x) for x in vecx ]
@df df boxplot(sprintf02i(:id), :value, fillalpha=0.75, linewidth=1)

The above two lpad and sprintf workarounds work well for this particular example. But what if I want the order to be 21 3 17? Of course, I can still find a workaround, but it does not look beautiful.

I happened to work a bit with boxplots yesterday. It would be really nice if there was a recipe for Pair{String, AbstractArray} so you could make a boxplot from ["a"=>[1,5,9], "b"=>[7,7,19]]. I might just write one.