How PDP added address validation behind a feature flag
We look at a slice of the PDP integration with an external address normalization service: why validation is optional, and manual entry remains the default flow.

3 optional integrations in PDP produce 8 states to check even before business scenarios: external CRM on or off, 1C on or off, address validation service on or off.
In this task, we connected an external service that validates and normalizes an address when creating a property object. On the form, the user enters an address, the system tries to normalize it through the service, and then the object is created with the normalized data.
But the service must not become a required part of the system. If it is turned off or unavailable, PDP keeps working: the address is entered manually, the property object is created, and the other integrations follow their own flags.
Why it wasn’t hardwired on
PDP already has several external touchpoints: CRM, 1C, and now address validation. All of them are optional. This matters not only for different system deployments, but also for fault tolerance: the unavailability of the address service should not block object creation.
There are two valid paths for an address in this logic:
- it goes through the external service and comes back in normalized form;
- it is entered manually because the integration is disabled or the service did not respond.
The second path is not treated as a user error. It is a normal fallback. That is why the flag should be checked before calling the external API, and an exception from the service should not be propagated upward as a reason to stop object creation.
In simplified form, this part looks like this:
const integrations = {
externalCrm: env.bool('EXTERNAL_CRM_ENABLED', false),
oneC: env.bool('ONE_C_ENABLED', false),
addressValidation: env.bool('ADDRESS_VALIDATION_ENABLED', false),
}
async function resolveAddress(input: AddressInput): Promise<AddressResult> {
if (!integrations.addressValidation) {
return {
source: 'manual',
address: input,
}
}
try {
const normalized = await addressValidationService.normalize(input)
return {
source: 'validation-service',
address: normalized,
}
} catch (error) {
logger.warn({ error }, 'Address validation service unavailable')
return {
source: 'manual',
address: input,
}
}
}
What matters here is not the variable names, but the responsibility boundaries.
The ADDRESS_VALIDATION_ENABLED flag only controls whether we attempt to call the external service. It does not change the property creation flow itself. The form and the domain logic get the result in a single format: there is always an address, only the source changes.
source is not there for decoration. The calling code should not have to guess where the address came from: from the service or from manual entry. This is especially useful alongside integrations where the same field may later go to an external CRM or to 1C.
A service error is not an object creation error
The most dangerous option is to tie validation availability to the ability to save the object. In that case, the external service becomes a critical dependency, even though its role is auxiliary: to suggest and normalize the address.
That is why catch falls back to manual mode. Yes, the address may remain exactly as the user entered it. But the user does not lose the object creation flow because of someone else’s API.
This is easy to get wrong in tests. At first, you want to check two cases: the flag is off and the flag is on. But the real matrix is wider, because CRM and 1C are also in the picture. That gives you at least 8 flag combinations:
- all integrations disabled;
- only CRM enabled;
- only 1C enabled;
- only address validation enabled;
- pairs of integrations enabled;
- all three enabled.
And that is without checking whether the address service is unavailable. The real pain shows up in exactly these combinations: locally it is easy to turn everything on at once and miss that the scenario “validation off, 1C on” follows a different branch.
What was tested separately
For this piece, the important thing is not the number of tests, but coverage of flag states and external API failure.
A minimal set worth keeping near this kind of integration:
- when
ADDRESS_VALIDATION_ENABLEDis off, the external service is not called; - when the flag is on, a successful service response returns a normalized address;
- when the service errors, the manual address is returned;
- other optional integrations do not change the decision about whether the object can be created.
The last point is boring, but it is exactly what protects you from combinatorics. Address validation should not suddenly depend on whether data is being sent to an external CRM or to 1C. The next step for this kind of integration is to keep the flag matrix explicit, rather than reconstructing it from memory before release.