However, what is stopping someone, for example the recipient of the original transaction, to simply send in the same exact transaction with the same data and signature?
Bitcoin does not have the concept of account balance but works on the concept of unspent transaction outputs (UTXOs). Each output of a transaction (except
OP_RETURN ones) lead to the creation of a separate UTXO. When you create transactions in Bitcoin, you consume these UTXOs in entirety and create separate UTXOs. This is done by referencing the outpoint (
n) from which you 'earnt' these UTXO. For example, if you control 2 UTXOs (1 BTC and 0.5 BTC), and you want to send your friend 1.25 BTC, you would have to consume both your UTXOs and send the remaining change of 0.25 BTC to yourself (neglecting tx fees). So if the recipient of a transaction output tries to broadcast the same transaction again, the Bitcoin nodes when verifying the transaction would find that these utxos do not exist and hence the transaction is invalid.
When full nodes start syncing from the Genesis block, they start to build the database of all these UTXOs. Every transaction involves removing spent UTXOs and adding new ones. These database is stores in
chainstate directory and aggressively cached in memory.