Basic structure for interactive single window application
The code looks decent enough.  I'd like to point out three issues, though, all related to timing/multi-threading.

GUI &rarr; EDT

All GUI operations should happen on the Event Dispatch Thread.  Java boots into the Main thread, which is different from the EDT.  Your Controller.getInstance() should be called on the EDT:

EventQueue.invokeLater(Controller::getInstance)


Controller.instance should be guarded

Currently, the instance if filled in via the Main thread.  If the GUI (on the EDT or through the timer) later requests the instance, there is no guarantee they will see it, or they might see it in an inconsistent/incomplete state.

This can be solved by either making instance volatile or, perhaps better, making getInstance() a synchronized method.

repaint() is on a best-effort basis

It looks like this is the basis for a game or other graphically heavy application (judging from FPS).  Swing (and its timers) may not give you the timing guarantees you'd need for things like game logic.

Consider using a game loop with a Canvas and a BufferStrategy, disabling redraw requests, and taking over from there.

Isn't the Timer good enough?  I'm tempted to say, yes, it might be good enough, if the game logic is simple and not intense.  Though you're no longer blocking the EDT with Thread.sleep (big win), the actions that the timer calls are still executed on the EDT.  This makes handling GUI elements and fields much simpler (no sync needed), but heavier stuff may result in jitter.  Worse: heavy stuff on the EDT may make your game loop jitter.

In short: the timer is simpler and probably good enough.  The game loop in a separate thread (say, your main thread) is more heavyweight but also more reliable and more likely to benefit from hardware acceleration.



Nitpicking


Controller.getInstance seems like an odd method to call from main to kick things off.  Maybe make it Controller.run() or Controller.getInstance().run() or some such that would better imply the side effect of running your program.
getPreferredSize returns a mutable final variable (Dimension is mutable).  Perhaps you could try this:

public Dimension getPreferredSize() {
    return isPreferredSizeSet() ? super.getPreferredSize() : new Dimension(DIMENSION);
}