private void addConfigInstances(
            final AbstractConfig config,
            final Prop parentProp,
            final Map<Class<? extends AbstractConfig>, AbstractConfig> configInstanceMap,
            final ObjectMapper objectMapper,
            final PropertyPath propertyPath,
            final AtomicInteger changeCounter,
            final boolean logChanges) {

        try {
            final Class<? extends AbstractConfig> clazz = config.getClass();

            final Map<String, Prop> propMap = PropertyUtil.getProperties(objectMapper, config);

            propMap.forEach((k, prop) -> {
                final String childPropName = prop.getName();
                final PropertyPath childPropPath = propertyPath.merge(childPropName);
                final Class<?> valueType = prop.getValueClass();

                // Don't recurse into non-injectable values, treat them like a non config type
                if (AbstractConfig.class.isAssignableFrom(valueType)
                    && !valueType.isAnnotationPresent(NotInjectableConfig.class)) {

                    final AbstractConfig childValue = (AbstractConfig) prop.getValueFromConfigObject();

                    if (childValue != null) {
                        // recurse
                        addConfigInstances(
                                childValue,
                                prop,
                                configInstanceMap,
                                objectMapper,
                                childPropPath,
                                changeCounter,
                                logChanges);
                    } else {
                        if (!prop.getValueClass().isAnnotationPresent(NotInjectableConfig.class)) {
                            // We should not have null config objects that are meant to be injectable
                            throw new RuntimeException(LogUtil.message(
                                    "Prop {} is null but is injectable config",
                                    childPropPath));
                        }
                    }
                }
            });

            // Non-injectable config is only accessible by an injectable ancestor so no need to add
            // it to the providers map
            if (!clazz.isAnnotationPresent(NotInjectableConfig.class)) {
                // Add the injectable config object to the instance map so the providers can get it
                configInstanceMap.put(config.getClass(), config);
            }

            // If it is not in the map then it is not injectable
//            final AbstractConfig existingConfig = NullSafe.getOrElseGet(
//                    this.configInstanceMap,
//                    map -> map.get(clazz),
//                    ThrowingSupplier.unchecked(() -> clazz.getConstructor().newInstance()));
            final AbstractConfig existingConfig = NullSafe.get(
                    this.configInstanceMap,
                    map -> map.get(clazz));

            // This is an update so see what has changed
            propMap.forEach((propName, prop) -> {
                final PropertyPath childPropertyPath = propertyPath.merge(propName);
                final Class<?> valueType = prop.getValueClass();
                // Only log changes for values that are not config objects or config objects
                // That can't be injected.
                // Recursion into config objects will happen above
                if (!AbstractConfig.class.isAssignableFrom(valueType)
                    || valueType.isAnnotationPresent(NotInjectableConfig.class)) {
                    final Object newValue = prop.getValueFromConfigObject();
                    Object existingValue = null;
                    if (existingConfig != null) {
                        existingValue = prop.getValueFromConfigObject(existingConfig);
                    }

                    if (existingValue == null && AbstractConfig.class.isAssignableFrom(valueType)) {
                        // Create a vanilla config obj with all defaults
                        LOGGER.debug("Using vanilla object for property {}, class {}", childPropertyPath, valueType);
                        existingValue = ThrowingSupplier.unchecked(() ->
                                        valueType.getConstructor().newInstance())
                                .get();
                    }

                    NullSafe.get(prop.getParentObject(), PropertyUtil::getProperties);
                    if (!Objects.equals(existingValue, newValue)) {
                        checkForValueChanges(
                                childPropertyPath,
                                prop,
                                parentProp,
                                newValue,
                                existingValue, logChanges,
                                changeCounter);
                    }
                }
            });
        } catch (Exception e) {
            throw new RuntimeException("Error adding config instances for " + config.getClass().getName(), e);
        }
    }
This is sort of the equivalent to stroom's ConfigMapper class.
It holds the de-serialised config objects in a map that is then used to
provide them for guice injection.
stroom-proxy/stroom-proxy-app/src/main/java/stroom/proxy/app/guice/ProxyConfigProvider.java