Search is normally simple enough. You need a way to sort generated moves, which is usually done by MVV/LVA and SEE for the captures, which also distinguishes good and bad captures, and killer (and possibly history) for the non-captures. You need a mechanism to put the hash move in front of all that, or do internal iterative deepening when you have none. Then you need null move pruning and a general mechanism for LMR, which can be tuned through tables that decide how much you should reduce as a function of remaining depth and move number. You should have in-check detection and the possibility to extend (overruling any reductions) the evasions. And you should of course do a QS where stand-pat is possible as a proxy for all non-captures, and good captures are 'extended' indefinitely.
That covers about it. Things like PVS or aspiration windows only have minor impact. Futility pruning would probably have a bit larger impact, but is is also something that (like raw nps speed) is just a one-time gain, and which would not change the effective branching ratio of your search. The key issue is to get that branching ratio low without overlooking too much.
King Slayer is a practical example of a very basic engine that beats Fairy-Max by ~85%, despite the fact that it does have the same extensions and reductions (null-move R=2, and 1 ply reduction of non-captures. Although I think Fairy-Max exempts all Pawn moves from LMR, while King Slayer does not do that. But it exempts killers, which Fairy-Max does not have at all.) King Slayer and Fairy-Max also have roughly the same, 'knowledgeless' evalution: PST that draw minors and Kings to the center, and push Pawns forward (especially to 6th and 7th rank). The reason King Slayer beats Fairy-Max is purely due to the larger depth it achieves through better move ordering (MVV/LVA + killers).
What do you think is not simple about it? It is just a loop over depth, starting at 1. The best move of the iteration becomes hash move of the next (i.e. it is placed in front of the move list) at the end of the iteration. That is all.
I guess it depends on what your threshold is for calling something complex. Search will obviously never be as simple as a "hello world" program.
Aborting the search never struck me as a problem. I just have a global 'abortFlag', and I test it directly after UnMake() to do an instant return when it is set. In King Slayer/Simple the abortFlag is only set if the search time exceeds the 'panic' limit, which is probed every 1024 nodes to not loose too much time on clock reading.