This is regular vi compatible behaviour. After pressing Esc the cursor is always moved one character back. I believe the reason is the following: If the cursor in insert mode is at the end of the entered text and you press Esc, there is no text on which to place the cursor, therefore it is moved back to the last entered character. The one exception to this rule is, if the cursor is on the start of the line, in which case it obviously cannot be moved to the left and will be placed on an empty cell (press ga on it and notice the NUL
result, which basically means there is no character)
If you don't want that behavior, I would suggest to map it to a behavior that you want, e.g.
inoremap <silent><expr> <esc> col('.')>1&&col('$')<col('.')?'<esc>l':'<esc>'
This uses an expression mapping that checks whether the cursor is either after column 1 or before the last column in the current row and only in those cases will try to move the cursor after pressing Esc in insert mode back to the character it was in insert mode.
Slightly related, you might want to also allow to put the cursor after the last character. This can be done using :set ve=onemore
. In that case, you do not need the condition &&col('$')<col('.')
above and you can skip that.