Serial.write is synchronous
Until the buffer is full, Serial.write is asynchronous.  You just have to avoid filling the buffer up.

Fortunately, there is the function Serial.availableForWrite() which returns the number of bytes available in the buffer. You can use that to see if there's room in the buffer for what you want to send, and if there is then send it (i.e., just placing it in the buffer).

By re-writing the UART handing to call your own interrupt routine to send a byte as and when needed you are basically doing exactly the same as the Arduino's HardwareSerial class is doing - just with your own buffer instead of the one in the HardwareSerial class.

There is no hardware buffer in the Arduino. It only has a single byte shift register for sending UART data. Any buffering has to be done in software, so either you implement that yourself, or use the existing buffering, or only ever generate and send one byte at a time, which is fairly useless.

If you really want to re-write it then all the information you need is in the datasheet for the MCU on your chosen development board.



The terms "synchronous" and "asynchronous" when applied to situations like this are better replaced with "blocking" and "non-blocking", since "synchronous" and "asynchronous" when applied to serial communications imply different styles of data transmission, not software implementation.