There are two approaches that are used.
In the past, it was common to use hardware flow control. This uses an additional wire in each direction. The sender waits until the wire indicates the receiver is ready. When the receiver is not ready to receive data, it signals the other side. Hardware would buffer at least one byte and, if the buffer was full, signal the other side not to send over this wire.
This is less common today. UARTs are so slow relative to modern hardware and large buffers are so cheap and easy to provide that there is no longer an issue. The sender just fills the receiver's hardware buffer and the receiver empties the hardware buffer periodically. Software would have to ignore the buffer for a long time for it to overflow.
An intermediate solution is to use flow control in the data flow. Generally, two characters are reserved, one to stop the flow and one to resume it. The receiver sends a flow control character to the sender if its buffer is getting close to full and another one if its buffer is getting close to empty. This is really only useful if the data flow doesn't need to handle binary data. This is extremely rare and was traditionally used primarily for connections that had a human on one end. You could also pause the flow if the information was coming faster than you could read it.
Generally, the protocols used are tolerant of overflow and include some form of high-level acknowledgement and/or retransmission as appropriate. One device might wait for the other side to send some kind of response to its command and, if it doesn't get one, retry the command. The protocol is designed not to do anything terrible if a command is received twice since it might be the reply that's lost.