Hi Mark — for
upgrade-safe multi-company synchronization in Dynamics 365 Business Central, I’d avoid “hard” database-style replication and design it as an
integration pattern with clear ownership, delta tracking, retry handling, and auditability.
Short recommendation
For shared master data such as
customers, vendors, items, dimensions, payment terms, posting groups, and units of measure, the best long-term patterns are:
| Scenario | Recommended approach |
|---|
| Multiple BC companies in same tenant | AL extension with an outbox/inbox sync pattern, processed by Job Queue |
| BC plus Power Platform / Sales / other Dynamics apps | Dataverse integration |
| External systems or cross-environment sync | Business Central APIs / custom API pages |
| Read-only shared data exposure | API queries, Dataverse virtual tables, or external reporting layer |
| True user-facing “single source of truth” | Central master-data company or Dataverse hub |
Do not directly modify Microsoft base objects or rely on unsupported SQL/database replication. Keep the solution in AL extensions, APIs, events, and Job Queues.
1. APIs vs. integration tables vs. Dataverse
Option A — Business Central APIs
Use this when the sync boundary is
outside the current company/session, across environments, or when you want a clean service interface.
Good for:
- Cross-environment synchronization.
- Integration with external systems.
- Upgrade-safe contracts.
- OAuth/service-to-service authentication.
- Clear separation of business logic.
Recommended patterns:
- Use standard APIs where possible.
- Create custom API pages for custom fields or entities.
- Use
SystemId as the API key where possible.
- Avoid exposing UI pages as OData services for high-volume sync.
- Use
$filter, $select, $top, and paging for deltas.
- Use retry logic for
429, 503, and 504 responses.
Best when:
- You want loose coupling.
- You need environment-to-environment sync.
- You want integration users and API permissions to control access.
Option B — AL-based company-to-company sync
Use this when all companies are inside the same BC environment and you need tight control over company-specific rules.
Good for:
- Shared master data inside one tenant.
- Company A → Company B/C/D propagation.
- Controlled synchronization from a “master company.”
- Custom validation and transformation logic.
Typical AL approach:
- Subscribe to table events on
Customer, Vendor, Item, Dimension, etc.
- Write a row to a custom Sync Outbox table.
- Let a Job Queue process the outbox asynchronously.
- Use
ChangeCompany() carefully when writing to target companies.
- Store sync status, retries, errors, timestamps, and source company.
Important warning: ChangeCompany() respects permissions, but table triggers still execute in the current company context. That means you must be very careful with business logic that depends on
CompanyName, setup tables, number series, posting groups, or dimensions.
Best when:
- Sync is mostly internal to BC.
- You need full AL control.
- You want near real-time but not blocking user transactions.
Option C — Dataverse
Use Dataverse when you need a
central hub for shared master data, Power Platform, Sales, Customer Service, or multi-app governance.
Good for:
- Centralized customer/account master.
- Integration with Dynamics 365 Sales.
- Power Automate workflows.
- Multi-environment or multi-company coupling.
- Match-based coupling and controlled synchronization.
Limitations:
- Dataverse sync is usually near real-time, not guaranteed instant.
- It depends on mappings, couplings, filters, and Job Queue processing.
- You must carefully manage company IDs and filters in multi-company scenarios.
Best when:
- Dataverse is already part of the architecture.
- Users outside BC also maintain master data.
- You need governance, approval, and Power Platform integration.
2. Recommended architecture for multi-company BC sync
I’d use this pattern:
Procedure: Build an upgrade-safe sync framework
- Define the source of truth.
Decide whether master data is owned by one BC company, each company, Dataverse, or an external MDM system.
- Split global fields from local fields.
For example, customer name and VAT number may be global, but posting groups, dimensions, credit limits, tax areas, and payment methods may be company-specific.
- Add a stable global identifier.
Add a custom field such as Global Master Data Id: Guid to shared master tables.
- Create a mapping table.
Store source company, target company, source SystemId, target SystemId, entity type, sync status, and last sync timestamp.
- Create an outbox table.
Store pending sync work instead of syncing immediately inside table triggers.
- Subscribe to table events.
Use events such as insert, modify, rename, and delete events to enqueue work.
- Process outbox rows with Job Queue.
The Job Queue should process records in small batches and log success or failure.
- Use idempotent upsert logic.
Running the same sync message twice should not create duplicates or corrupt data.
- Prevent sync loops.
Mark changes made by the sync engine so that target-company updates do not re-enqueue endless reverse changes.
- Add telemetry and admin pages.
Provide pages for pending records, failed records, retry count, last error, and manual reprocessing.
3. Handling conflicts and duplicate records
This is where many sync projects fail. Do not rely only on
No..
Recommended approach:
- Use a global GUID for each shared master record.
- Keep local
No. values if companies have different number series.
- Store cross-company mapping in a custom table.
- Use match rules during initial coupling.
- Use deterministic conflict resolution.
Good matching fields:
- Customer: VAT Registration No., registration number, normalized name, email, phone.
- Vendor: VAT Registration No., bank account, normalized name.
- Item: GTIN, vendor item no., manufacturer code, item reference.
- Dimension: code plus dimension type/global GUID.
- Contacts: email plus company/contact relationship.
Conflict strategies:
| Conflict type | Recommended handling |
|---|
| Same record changed in two companies | Use source-of-truth rules or field-level ownership |
| Duplicate candidate found | Put into review queue, do not auto-merge blindly |
| Target record missing | Create only if match rules pass |
| Target record blocked | Skip and log error |
| Local-only field changed | Do not overwrite from master |
| Delete from source | Usually block/delete-mark instead of hard delete |
For master data, I usually recommend
field ownership rather than “last write wins.”
Example:
| Field | Owner |
|---|
| Name | Master company |
| Address | Master company or Dataverse |
| Customer Posting Group | Local company |
| Gen. Bus. Posting Group | Local company |
| Payment Terms | Master or local, depending on policy |
| Default Dimensions | Often local |
| Blocked | Usually local |
4. Performance considerations for large datasets
For large data volumes, design for
delta sync, not full sync.
Recommended patterns:
- Process only changed records.
- Store a last processed timestamp/version/hash.
- Use
SetLoadFields() when reading only selected fields.
- Use
FindSet() with filters and limited batch sizes.
- Avoid heavy logic in page triggers or API pages.
- Avoid synchronous cross-company writes inside user transactions.
- Process in batches, for example 100–500 records per run.
- Commit at safe boundaries, not after every field change.
- Use separate Job Queue categories per entity if needed.
- Monitor locks, deadlocks, and long-running sessions.
For APIs:
- Prefer API pages/API queries over exposed UI pages.
- Use
$select to limit columns.
- Use
$filter for changed records.
- Use
$top and paging.
- Use
$batch where appropriate.
- Handle throttling with exponential backoff.
- Do not run massive full syncs during business hours.
For initial load:
- Take a backup or test in sandbox first.
- Run matching/coupling before insert.
- Import in entity dependency order.
- Start with dimensions, posting groups, payment terms, units of measure.
- Then sync items, vendors, customers.
- Then enable incremental sync.
5. Job Queues and background sessions
For near real-time sync, I recommend
event-driven enqueue + scheduled background processing.
Better pattern
- User modifies a customer.
- Event subscriber detects relevant change.
- Subscriber writes one row to
Sync Outbox.
- Job Queue runs every 1–5 minutes.
- Job Queue processes pending rows.
- Errors are logged and retried.
This avoids blocking the user session and makes failures recoverable.
Avoid this pattern
Do not do heavy cross-company synchronization directly inside
OnAfterModify or
OnAfterInsert.
Problems with direct sync:
- Slows down user posting/data entry.
- Increases lock duration.
- Makes conflicts harder to recover.
- Can create recursive sync loops.
- Can fail the user’s transaction because another company has invalid setup.
Job Queue design
Use separate processors:
Customer Sync Processor
Vendor Sync Processor
Item Sync Processor
Dimension Sync Processor
Or one generic processor with parameters:
Code:
codeunit 50100 "Master Data Sync Processor"
{
TableNo = "Job Queue Entry";
trigger OnRun()
begin
case Rec."Parameter String" of
'CUSTOMER':
ProcessCustomers();
'VENDOR':
ProcessVendors();
'ITEM':
ProcessItems();
'DIMENSION':
ProcessDimensions();
end;
end;
}
Expected outcome:
- Users are not blocked by sync work.
- Failed records can be retried.
- Admins can review and correct issues.
- Sync can be paused safely.
6. Security and business rules
Security must be designed intentionally.
Recommended security model:
- Use a dedicated sync user or service principal.
- Give only required permissions.
- Use permission sets specific to sync objects and target tables.
- Respect company access restrictions.
- Log the original user who caused the change.
- Never bypass validation unless you fully understand the consequences.
- Keep company-specific fields protected from global overwrites.
Business rule handling:
- Use
Validate() for fields where Business Central business logic must run.
- Use direct assignment only for technical fields where validation is not needed.
- Do not copy posting setup blindly across companies.
- Do not assume number series are identical.
- Do not assume dimensions are identical unless governed centrally.
- Treat blocked records, tax setup, currencies, and posting groups as local unless agreed otherwise.
Important: If you use
ChangeCompany(), test permissions and trigger behavior carefully. A sync user must have rights in every target company.
7. Recommended AL development patterns
Use these patterns for extensibility and upgrade safety.
Use an outbox table
Example fields:
Code:
Entry No.
Entity Type
Source Company
Target Company
Source SystemId
Target SystemId
Operation Type
Status
Retry Count
Last Error
Created DateTime
Processed DateTime
Correlation Id
Payload Hash
Use a sync context/suppression flag
Prevent recursive sync loops.
Example concept:
Code:
codeunit 50101 "Sync Context"
{
SingleInstance = true;
var
IsSyncRunning: Boolean;
procedure SetSyncRunning(Value: Boolean)
begin
IsSyncRunning := Value;
end;
procedure GetSyncRunning(): Boolean
begin
exit(IsSyncRunning);
end;
}
Then in subscribers:
Code:
[EventSubscriber(ObjectType::Table, Database::Customer, 'OnAfterModifyEvent', '', false, false)]
local procedure CustomerOnAfterModify(var Rec: Record Customer; var xRec: Record Customer; RunTrigger: Boolean)
var
SyncContext: Codeunit "Sync Context";
begin
if SyncContext.GetSyncRunning() then
exit;
if not RelevantCustomerFieldsChanged(Rec, xRec) then
exit;
EnqueueCustomerSync(Rec);
end;
Use idempotent upsert
Pseudo-flow:
Code:
Find target by Global Master Data Id
If found, update allowed fields
If not found, try match-based coupling
If still not found, create new record
If multiple matches, mark conflict
Use field-level mapping
Do not copy every field.
Code:
Customer.Name -> sync
Customer.Address -> sync
Customer.Customer Posting Group -> local only
Customer.Gen. Bus. Posting Group -> local only
Customer.Blocked -> local or controlled by policy
Use integration events in your own app
Expose events such as:
Code:
[IntegrationEvent(false, false)]
local procedure OnBeforeApplyCustomerSync(var Customer: Record Customer; SourceCompany: Text; var IsHandled: Boolean)
begin
end;
[IntegrationEvent(false, false)]
local procedure OnAfterApplyCustomerSync(Customer: Record Customer; SourceCompany: Text)
begin
end;
This lets future extensions add rules without modifying your sync engine.
8. Entity-specific guidance
Customers
Recommended:
- Sync name, address, phone, email, VAT registration no.
- Keep posting groups and dimensions local unless standardized.
- Use global ID plus VAT/tax registration matching.
- Handle customer templates carefully.
Vendors
Recommended:
- Sync name, address, VAT registration no., contact info.
- Treat bank accounts as sensitive and permission-controlled.
- Keep posting groups, payment methods, and dimensions local unless governed.
Items
Recommended:
- Sync description, base unit of measure, GTIN, item category, tracking policy if standardized.
- Be careful with costing method, inventory posting groups, tax groups, and replenishment settings.
- Do not sync inventory quantities as master data.
Dimensions
Recommended:
- Centralize dimension definitions first.
- Sync dimension values before syncing default dimensions.
- Decide whether default dimensions are global or company-specific.
- Avoid deleting dimension values that may be used in posted entries.
9. Practical “best” architecture
For most multi-company Business Central projects, I’d implement this:
- Master data company owns shared records.
- Custom AL extension adds global IDs and sync tables.
- Event subscribers enqueue changes.
- Job Queue processes changes every few minutes.
- Field mapping setup controls which fields sync.
- Conflict queue handles duplicates and ambiguous matches.
- Per-company setup controls target companies and local rules.
- Telemetry and admin pages show health, errors, and retry status.
- Dataverse is used only if Power Platform or cross-app MDM is part of the requirement.
- APIs are used for cross-environment or external integrations.
10. What I would avoid
Avoid these if you want the solution to remain upgrade-safe:
- Direct SQL replication.
- Modifying base application objects.
- Copying complete records blindly between companies.
- Synchronous sync inside table trigger logic.
- Using only
No. as the cross-company identity.
- Hard deleting master data across companies.
- Treating posting setup and dimensions as universally identical.
- Exposing UI pages as high-volume OData endpoints.
- Running full sync repeatedly instead of delta sync.
- Ignoring conflict handling until after go-live.
Final recommendation
For your scenario, the most robust solution is:
Use an AL extension with an outbox/inbox pattern, global record IDs, field-level ownership, Job Queue processing, and conflict management. Use
Dataverse if you need a central enterprise data hub or Power Platform integration. Use
APIs when synchronization crosses environments, tenants, or external systems.
This gives you:
- Upgrade safety.
- Auditability.
- Retry handling.
- Better performance.
- Clear business-rule boundaries.
- Reduced risk of duplicate or corrupted master data.