The first integration you build for a client is, by definition, bespoke. You're reading their platform's quirks, their booking rules, their record structure, and writing code that fits all of it. It works, and it should: a single deployment doesn't need to be general, it needs to be right.
The question changes the moment a second client shows up with a different platform and a similar shaped problem. Do you write the integration again from scratch, or was there a reusable core hiding inside the first one? I've been on the second side of that question recently, and the honest answer is: some of it was hiding there, and some of it wasn't, and telling the two apart is most of the work.
Most of what looks like logic is actually data
The first surprise is how much of what I'd written as code was really configuration wearing code's clothes. Package rules, what a service includes, which options are exclusive versus additive, how a bundle prices out: none of that is business logic in the sense of "reasoning the system has to do." It's a description of one client's catalogue. The engine that reads a catalogue and enforces its rules is genuinely reusable. The catalogue itself never was, and was never supposed to be.
Once I saw that split, the rewrite wasn't really a rewrite. It was pulling the catalogue out into its own file and checking that the engine underneath had no client-specific assumption baked into it. Mostly it didn't. Where it did, that's the real work.
Draw the line at the operation, not the platform
The temptation is to generalise around "the platform," building an abstraction that hides which booking system you're talking to. That's the wrong seam. Platforms differ in ways too specific to paper over cleanly, and you end up with a leaky abstraction that's harder to reason about than two honest integrations would have been.
The seam that held up was the operation: read availability, hold a slot, confirm a booking, reschedule it, cancel it. Each of those has a stable contract regardless of which system fulfils it underneath. The reusable core is the set of operations and what's allowed to call them. What's swappable is the platform-specific code behind each one, and that code is allowed to stay ugly and specific, because it's small and isolated rather than smeared through the whole build.
You don't know the real boundary until the second case
This is the one I'd tell someone starting their first integration. You cannot design the generic version from a single deployment, because you don't yet know which decisions were forced by the problem and which were just how this client happened to do it. The first build teaches you the shape. The second build is what corrects it, because it disagrees with the first one in exactly the places where you'd guessed wrong.
That's not a failure of planning. Building for reuse before you've seen a second real case usually produces the wrong abstraction, one shaped by imagination rather than by an actual second set of constraints.
Authority has to be re-earned, not inherited
The part that doesn't generalise for free is what the system is trusted to do on its own. On the first build, I'd drawn a careful line: the assistant can reschedule or cancel an existing, already-verified booking without asking for identity again, because that person was checked once already. Creating a new booking that depends on verifying who someone is stays on a form, not a free-text conversation.
That line doesn't carry over automatically just because the operations look the same on a new platform. Each deployment gets its own pass on what's safe to automate, because the verification a new platform actually performs, and what it lets you check on the assistant's behalf, isn't guaranteed to match the first one. Reusing the code without re-deriving the authority would be reusing a conclusion without checking whether the reasoning behind it still holds.
The config file is the interface now
The practical result is that adding a client stopped looking like an integration project and started looking like a configuration exercise: describe the catalogue, describe what this deployment is allowed to automate, point the operations at this platform's specifics. The engine underneath doesn't change. If it needs to change to fit a new client, that's a sign the last generalisation wasn't finished, not that this client is a special case.
That's what "reusable" actually means in practice. Not one integration copy-pasted everywhere, but one engine and a growing set of configurations that describe how each deployment differs from the last.
That shift changed how I describe the work. The engine and the catalogue modules are mine, and they get sharper every time a new clinic surfaces an edge the last one didn't have. A new deployment is a configuration of that engine, not a fresh build. And it doesn't end at go-live: prices move, packages change, a branch gains equipment it didn't have in March. Keeping each configuration true to the clinic it describes is the recurring work, and it's the part that decides whether the rest of it stays correct.
Related reading: from answering to acting on the first build this one generalises, and the security pass before write access on why the authority a deployment holds gets re-derived rather than inherited.