Essentially you're trying to implement a two-phase commit transaction with Salesforce yet that feature as of this writing is not supported.
Your next best option is a formal integration pattern that can capture errors (e.g. in a custom object) and report / notify people as-needed when the integration fails at any point so data inconsistencies can be fixed. So you'll really be making trade-offs on which system needs to have the change occur first and rely on that response on next actions.
Salesforce Integration Design Patterns
Please see Chapter 2: Remote Process Invocation - Request and Reply of the Salesforce Integration Patterns and Practices. This talks through similar process as you've described where a Visualforce page's controller will make Http Callout to external system and receive response.
Http Callouts and Uncommitted Work
Since Http Callouts cannot be done if any DML has occurred prior to the call, you will either need to:
- make the http callout before you do Salesforce DML, or
- do the Salesforce DML then perform http callout asynchronously such as Queueable job or @Future method
See this help article with more details on the use case and example code.
In case of update to any contact what could be the best approach. Update operation can be performed from Visualforce or standard UI. Please note, we need to make a single synchronous call to external system.
Option 1: I would make the http callout, if successful, then continue with DML operation in Salesforce, else display error to user. This prevents Salesforce data from being created unless external system is successful.
Option 2: Save data to Salesforce immediately. Use near real-time integration (batchable or middleware) to query for un-synced records and post to external system keeping log of any integration errors. If errors, escalate to proper teams to investigate. Salesforce user-experience will be faster, non-blocking, and external system will eventually catch up.
In case of deleting a contact what could be the best approach. Delete operation will be perform from salesforce standard UI.
Option 1: If deletion occurs from standard delete API or standard delete button then you will only know this is happening in an apex trigger. At this point you have no opportunity to make a real-time http callout due to uncommitted work. You would have to use asynchronous apex to make http callout. The record in Salesforce would be deleted and external system may or may not be successful. You can use the undelete API method in Salesforce to restore the data if necessary, but I would worry more on why external system failed to delete and keep pushing forward, not undo it.
Option 2: Override the standard Delete action with a custom Visualforce page and Apex class. Make the http callout first, if successful then perform delete DML in apex, else display error to user. This prevents Salesforce data from being deleted unless external system is successful.
Option 3: If external system is the master in your Master Data Management scenario, then deletes in Salesforce should only ever be supported as a request to delete and not allow people to physically delete in Salesforce outside of an integration user doing the DML delete operation triggered by external system.
Personal Preference if Salesforce is "Source of Truth"
I prefer to use near real-time integrations where the external system is eventually consistent with data mastered in Salesforce.
To achieve this, I would use Batchable Apex or Middleware ETL to query for inserted/updated/deleted records using below methods then make necessary Http Callouts. Ideally, the end system could receive bulk records and perform changes all at once so as to reduce the chattiness of the integration. You would need to store in perhaps a custom setting the last timestamp when you ran these methods so that you can use it as the next start time in the next calls.
Database.getUpdated( sobjectType, startDateTime, endDateTime ) and
Database.getDeleted( sobjectType, startDateTime, endDateTime )
SOAP API Documentation.
Also be thinking about if an error occurs and a record fails to sync then it may be left out of the next window when you call getUpdated / getDeleted. Error records may need to be re-processed in a separate batch scenario, which means you'd need a flag on the record or in some tracking object to know which records are failing and still need to be resolved.