The Integration Boundary
When two systems don't speak the same language
By this point, we had a fairly complete inspection workflow inside our own platform.
Data was collected in the field.
Images were processed and correlated to inspection assets.
Inspectors could review the results and annotate anomalies.
That covered the part of the workflow we controlled.
At some point, though, those annotations had to leave our system.
They needed to be sent to a third-party platform used for the next stage of the process.
At first, that sounded straightforward.
We had the annotation.
They had a place for it.
We just needed to move the data across.
That was not quite how it worked out.
The first thing that became clear was that the two systems were speaking different languages.
Our internal model had its own concepts.
The third-party platform had its own concepts.
Some of them lined up reasonably well.
Others only overlapped partially.
And some of them didn't line up at all.
An asset in our system was not necessarily represented the same way externally.
The same was true for annotations and the relationships between them.
That pushed me to think more carefully about where the integration should actually live.
I didn't want the rest of the platform to start carrying third-party assumptions just because we needed to send data somewhere else.
So we kept that logic behind its own boundary.
The integration ended up living in a separate service.
That service was responsible for translating our internal data into the external format, keeping track of the external entities, and handling the communication with the other platform.
That separation made the rest of the system easier to work with.
Our core domain could keep thinking in terms of inspections, assets, and annotations.
The integration service dealt with the external model.
The mapping happened at the boundary between the two.
That turned out to be important.
Since the two systems maintained their own identities for the same entities, we needed a way to bridge them.
Our assets had internal IDs. The external platform had its own IDs. For every entity that needed to be synchronized, we stored the relationship explicitly rather than trying to infer it later.
Something like:
Internal Asset
↓
External Asset ID

It seems obvious once it's in place.
At the time, it was one of those details that could quietly create a lot of confusion if you didn't get it right from the start.
Especially once synchronization stopped being a one-time thing.
If an annotation was exported once, we needed to know where it lived externally.
If it was retried, we needed to know whether we were creating something new or updating something that already existed.
That mapping became part of the system, not just part of the export.
The same thing happened with the data transformation itself.
I didn't want the integration details leaking into the rest of the application.
So instead of scattering external-specific logic around the codebase, we kept the translation in one place.
The adapter layer was where our internal model was converted into the external format.
It handled the field mapping.
It handled the differences in terminology.
It handled the pieces that didn't line up exactly.
That way, the rest of the platform could keep dealing with our own concepts without needing to know too much about the third-party system.
I found that much easier to reason about than trying to make both systems look the same.

Reliability was the other part that needed attention.
Once the integration was in place, it was easy to assume that a successful request meant the job was done.
In practice, it wasn't that simple.
Requests could fail.
The external system could be unavailable.
A timeout could happen after the other side had already processed the data.
A retry could send the same annotation again.
So we had to keep track of what had been sent, what had succeeded, what had failed, and what needed another attempt.
That ended up being just as important as the data mapping itself.
Without that, it would have been too easy to lose track of what had actually made it to the other side.
I remember the implementation growing a bit more deliberate over time.
The integration service became the place where we managed the translation, the identifiers, and the reliability controls.
That kept the rest of the platform simpler.
It also meant that when the external system changed, we had one place to adjust instead of having that logic spread across the application.
That was probably the main reason it held up as well as it did.
Looking back, this was one of the first projects where I really appreciated how different systems integrations are from the rest of an application.
Inside our own platform, we controlled the model, the storage, the queues, and the deployment. The integration was different because none of those decisions were ours alone. Every change had to respect another system's model, identifiers, and constraints.
That separation ended up paying for itself many times over. As both systems evolved independently, having a clear boundary meant most of the changes stayed exactly where they belonged.