- Start from the write path
- Send an event, not the whole customer
- Define the integration contract before coding
- Add an outbox before adding more integrations
- Handle failures where they actually happen
- Do not use scheduled polling as the primary trigger
- Keep the Boomi process small
- Decide who owns every field
- Make retries safe
- Do not bypass Delphi business rules on the way back
- Keep runtime close to SQL Server
- Separate the initial load from live synchronization
- What stays in Delphi and what moves to Boomi
- The result
Start from the write path
When integrating Delphi with Boomi, you should start not with the connector, but with the point where the application is already guaranteed to save data.
The VCL client communicates with SQL Server via ADO. The client is saved via AddUpdateClient, and the contact is saved via ContactAddUpdate. These procedures are more important than the form, because the form is only one of the ways to save data. An Excel import, a batch process, or a separate utility can bypass TformClientEdit.ClientSave but still write to the same tables.
Here is a snippet of the existing code:
| 12345 | DM.ADOStoredProcAddUpdateClient.Parameters.ParamByName('@Name').Value := Trim(txtName.Text);
DM.ADOStoredProcAddUpdateClient.Parameters.ParamByName('@Phone').Value := txtPhone.Text;
DM.ADOStoredProcAddUpdateClient.Parameters.ParamByName('@Email').Value := txtEmail.Text;
DM.ADOStoredProcAddUpdateClient.ExecProc; |
The first solution, therefore, is simple: do not place boomi integration around the Save button. Integration should be based on actual write paths: AddUpdateClient, ContactAddUpdate, TCLIENTS, TCONTACTS, and import.
This is the first practical step in legacy software modernization: the old code is not rewritten, but the system gains a stable integration boundary.
Send an event, not the whole customer
Delphi should not need to know the structure of the Salesforce Contact entity, nor should it retrieve a large JSON object containing all the customer’s fields. After a successful save, it is sufficient to send a brief event to Boomi indicating which entity has changed and what its ID is.
| 12345678910111213141516171819202122232425 | procedure NotifyBoomiChanged(const AEntity: string; AId: Integer);
var
Http: TIdHTTP;
Body: TStringStream;
begin
Http := TIdHTTP.Create(nil);
try
Body := TStringStream.Create(
'{"entity":"' + AEntity + '","id":' + IntToStr(AId) + '}',
TEncoding.UTF8
);
try
Http.Request.ContentType := 'application/json';
Http.Request.CustomHeaders.Values['x-api-key'] := GetBoomiApiKey;
Http.ConnectTimeout := 2000;
Http.ReadTimeout := 3000;
Http.Post('https://crm-runtime.company.local/ws/rest/legacy/change', Body);
finally
Body.Free;
end;
finally
Http.Free;
end;
end; |
The call is placed after a successful ExecProc. For a contact, this could be:
NotifyBoomiChanged('CONTACT', ContactId);
Boomi receives only CONTACT and FID, after which it generates Database Get on its own and reads the current values from SQL Server. If another field is needed in Salesforce tomorrow, the Delphi code remains unchanged: the mapping is extended in Boomi.
The boundary between the systems remains small. Delphi notifies of the change. Boomi knows where to send the data and how to transform it.
Define the integration contract before coding
Before making changes in Delphi, you should define the event contract. For the first release, two required fields are sufficient: entity and id. There’s no need to pass the name, phone number, email, and dozens of other attributes right away if Boomi already has access to SQL Server.
It is helpful to immediately define the valid values for entity, such as CONTACT and CLIENT, and to specify that id always corresponds to FID in the source table. Then the listener is not dependent on a specific version of Delphi, and a new source—whether an Excel importer, a service, or a batch script—can use the same contract.
The listener’s response should also be simple. A successful HTTP response simply means that Boomi has accepted the event for processing. It should not be used as part of the card save transaction. If Salesforce is temporarily unavailable, this is a delivery layer issue, not a reason to cancel AddUpdateClient.
For diagnostics in TSYNC_OUTBOX, it is sufficient to store the entity, entity ID, status, creation time, and the most recent error. There is no need to turn the outbox into a copy of the client: TCLIENTS and TCONTACTS remain the data sources.
Add an outbox before adding more integrations
An HTTP call offers speed but not reliability. The runtime may be unavailable. The network connection may be lost. An Excel import may save data to a location where you forgot to add a listener.
That’s why you need an outbox table next to the quick dial feature:
| 123456789 | CREATE TABLE dbo.TSYNC_OUTBOX (
FID INT IDENTITY(1,1) PRIMARY KEY,
FENTITY VARCHAR(20) NOT NULL,
FENTITYID INT NOT NULL,
FEMAIL NVARCHAR(320) NULL,
FSTATUS TINYINT NOT NULL DEFAULT 0,
FCREATED DATETIME NOT NULL DEFAULT GETDATE(),
FERROR NVARCHAR(400) NULL
); |
A trigger set to TCLIENTS or TCONTACTS simply adds a row to TSYNC_OUTBOX and then finishes running. It does not call Boomi or wait for Salesforce. Saving a customer should not depend on the availability of an external system.
The flow looks like this:
Delphi/Excel save → SQL Server → TSYNC_OUTBOX → HTTPS listener → Boomi Database Get → Salesforce lookup → create/update → sync status.
Each step accomplishes a single task: capturing a change, ensuring it is not lost, reading the current data, finding the associated record, and recording the result.
The fast track invokes the listener immediately. As a fallback, the Boomi process selects FSTATUS = 0 once per minute and retries processing. If the HTTP call is successful, the queue is confirmed. If not, the record remains available for recovery.
Handle failures where they actually happen
It is best to categorize errors by where they occur.
If Delphi is unable to call the listener, the card is still saved successfully, and the record is already in the outbox.
If Boomi cannot read SQL Server, the record remains unprocessed and will be reprocessed by the recovery process.
If Salesforce returns a validation error, Boomi logs the error as FERROR and sets the record to error status. There’s no need to keep repeating this process every minute: you should first correct the data or the mapping.
If an event contains an unknown entity, the process must terminate with a controlled error rather than attempting to guess the table.
This is enough for support to see the integration status directly in SQL Server: what is pending processing, what has been completed, and where there is an error. For the first release, this is more useful than a complex custom monitoring layer.
Do not use scheduled polling as the primary trigger
Polling is suitable as a fallback, but not as the primary mechanism, if a change needs to be applied within seconds. The old schema may not have a reliable FUPDATEDATE, so a request for “all records newer than the last run” will either miss changes or require a separate log.
The listener handles latency, while the outbox handles delivery. The scheduled process serves as a fallback and selects only unprocessed rows. This is simpler than regularly scanning TCLIENTS and trying to reconstruct the change history using data that wasn’t designed for that purpose.
If a full-fledged CDC is introduced later, it can be evaluated separately. For a small volume of changes, the outbox already provides a clear and controllable mechanism that does not depend on the transaction log.
Keep the Boomi process small
A custom Java connector is not required for this scenario. Everything needed is already in place: the Database Connector for SQL Server, the Web Services Server for the incoming call, and the Salesforce Connector for the target CRM.
A Boomi process consists of several steps.
1. Start / Web Services Server. Get entity and id.
2. Database Get. Using id, read data from TCONTACTS or TCLIENTS.
3. Validation. Check the required identifier. If the email address is used for matching and it is empty, do not create a Salesforce Contact. Log the error as TSYNC_OUTBOX.
4. Salesforce Get. Find an existing Contact based on the specified matching rule.
5. Decision. If the contact is not found, create it. If it is found, update only the fields that Delphi actually has access to.
6. Database Send. Save FSALESFORCEID, set FSTATUS = 1. If an error occurs, set FSTATUS = 2 and record FERROR.
The process does not involve any Delphi UI logic, nor does it call Salesforce directly from the legacy application. This is precisely what makes the boundary robust.
Decide who owns every field
Boomi and Salesforce integration becomes dangerous not because of the API, but because of unclear ownership. If both systems consider themselves the owners of the same field, the sync process begins to overwrite the data.
Minimum hand:
| Delphi / SQL Server | Salesforce | Rule |
| TCONTACTS.FEMAIL | Contact.Email | matching key |
| FSURNAME | LastName | write on create |
| FNAME | FirstName | write on create |
| Birthday | Birthdate or custom field | write on create |
| TCONTACTS.FID | Legacy_Contact_Id__c | always link |
| FPHONE1 | Phone | only if Delphi owns it |
| FSALESFORCEID | Salesforce Contact Id | technical link |
If Salesforce is the master source for a name, phone number, or status, there is no need to overwrite an existing Contact with data from Delphi. When creating the record for the first time, you can transfer the values; thereafter, you only need to update the technical relationship and the permitted fields.
FSALESFORCEID should be returned to the legacy database. This is not a full-fledged reverse synchronization of business data. It is a technical reference that allows the next event to immediately understand which Salesforce object the string is associated with.
The rule must be set before mapping. Otherwise, the map shape will appear correct, but the result will depend on which system wrote to the field last.
Make retries safe
The Outbox and recovery are only useful for idempotent processing. A single event may occur twice: the user saved the card again, the listener responded with a timeout after the actual processing, or a scheduled process retrieved the string at the same time as a quick call.
The duplicate should not create a second Contact.
Two levels of identification are used for this purpose. The first is a business key—for example, an email address, if that is the identifier used for matching. The second is a technical external ID—for example, TCONTACTS.FID—stored in Legacy_Contact_Id__c.
Processing logic:
– If a saved FSALESFORCEID exists, use it;
– Otherwise, search for a Contact based on the approved matching rule;
– If a Contact is found, link the legacy ID;
– If none is found, create a new one;
– After a successful operation, save the Salesforce ID in SQL Server;
– A repeat of the same event should result in the same record, not a new one.
There’s no need to “guess” empty or ambiguous keys. They should be handled in the exception flow. This is more cost-effective than correcting duplicates after the initial load.
Do not bypass Delphi business rules on the way back
The outbound flow from Delphi to Boomi is relatively simple: Boomi reads data that has already been saved. The reverse flow requires more caution.
In a legacy application, some rules may reside in forms, while others may be in stored procedures. For example, a form might check permissions, verify the uniqueness of a name, or display a warning about similar records. A direct UPDATE TCLIENTS will bypass these checks.
If a Salesforce → Boomi → Delphi integration is added later, you should make the changes via AddUpdateClient and ContactAddUpdate, rather than modifying the tables directly.
However, the outbound trigger must not respond to its own write operation and create a loop. One option is to mark the session or integration operation so that the trigger skips changes initiated by Boomi.
This is no longer just a matter of integrating two APIs. It involves preserving existing business rules while gradually modernizing the system.
Keep runtime close to SQL Server
If Boomi is to perform Database Get, the runtime must have access to SQL Server. There is no need to expose the database externally just for the sake of integration.
First, the runtime on which existing processes are already running is checked. If it has network access to SQL Server, the new process can be deployed there as well.
If there is no access, the local basic runtime is located next to SQL Server. Delphi clients send HTTPS requests to a shared listener; the runtime reads the database locally and connects to Salesforce via an existing external connection.
The runtime is not installed on users’ computers. Delphi remains a standard desktop application.
The connection to the database in Boomi remains a standard JDBC connection:
| 12345678 | <DatabaseConnectionSettings
driverId="sqlserver"
className="com.microsoft.sqlserver.jdbc.SQLServerDriver"
host="crm-sql.internal"
port="1433"
dbname="LegacyCRM"
username="boomi_sync"
isPoolEnabled="true" /> |
The boomi_sync account only needs minimal permissions: to read the necessary entities and the outbox, and to update the sync status and technical Salesforce ID. It does not need permissions for the entire database.
Separate the initial load from live synchronization
The initial load and live integration use the same mapping rules, but they are different processes.
If hundreds of thousands of records have already been accumulated in Delphi, the initial load is performed as a separate batch. First, a small sample is processed: existing Contacts, new Contacts, empty email addresses, duplicates, and field conflicts. After checking for matching and ownership, the main portion of the load begins.
After that, Live Flow operates on an event-driven basis and processes only new or modified records.
There’s no need to try to achieve real-time behavior by constantly polling the entire table. If the legacy schema does not include FUPDATEDATE, this type of polling does not even provide a reliable way to detect changes. A listener combined with an outbox solves both problems: fast startup and guaranteed delivery.
What stays in Delphi and what moves to Boomi
After integration, the boundary should be clear.
Delphi retains the existing forms, AddUpdateClient, ContactAddUpdate, local business rules, and integration with SQL Server.
Technical integration elements are being added to SQL Server: outbox, delivery status, and external Salesforce ID.
Boomi handles routing, mapping, validation, retries, the Salesforce connection, and the recovery process.
Salesforce retains its own data and ownership rules.
The following system connects to Boomi rather than being embedded directly in Delphi. The Salesforce API remains within the integration layer rather than being scattered throughout the VCL code. Delivery errors are visible in the queue. Retry processing does not create duplicates.
The result
This integration doesn’t require a large migration project or a Boomi custom connector.
We need five specific solutions: use Delphi’s actual write paths, send a small event, log changes to the outbox, determine field ownership, and make the processing idempotent.
After that, the process is straightforward:
Delphi save → outbox → Boomi → Salesforce → sync status.
This approach provides a practical foundation for software modernization services: the legacy application continues to run, but new integrations no longer require expanding its internal architecture.