You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@bookkeeper.apache.org by si...@apache.org on 2018/12/21 23:40:16 UTC

[bookkeeper] branch master updated: Introduce a configuration framework for better organizing and documentating configuration settings

This is an automated email from the ASF dual-hosted git repository.

sijie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/bookkeeper.git


The following commit(s) were added to refs/heads/master by this push:
     new 21d71fb  Introduce a configuration framework for better organizing and documentating configuration settings
21d71fb is described below

commit 21d71fb9e576d95e2ff2adf7a961a2847b96a9f9
Author: Sijie Guo <gu...@gmail.com>
AuthorDate: Sat Dec 22 07:40:11 2018 +0800

    Introduce a configuration framework for better organizing and documentating configuration settings
    
    Descriptions of the changes in this PR:
    
    ### Motivation
    
    One common task in developing bookkeeper is to make sure all the configuration
    settings are well documented, and the configuration file we ship in each release
    is in-sync with the code itself.
    
    However maintaining things in-sync is non-trivial. This proposal is exploring
    a new way to manage configuration settings for better documentation.
    
    ### Changes
    
    - Introduce `ConfigKey` for defining a configuration setting key in a configuration
    - Introduce `ConfigKeyGroup` for grouping configuration settings together
    - Introduce `ConfigDef` for generating the configuration definition for a given configuration
    - Add a `save` method for saving a configuration definition into a configuration file
    
    Master Issue: #1867
    
    
    
    
    Reviewers: Jia Zhai <None>, Enrico Olivelli <eo...@gmail.com>
    
    This closes #1869 from sijie/config_defs
---
 .../apache/bookkeeper/common/conf/ConfigDef.java   | 327 ++++++++++++++++++
 .../bookkeeper/common/conf/ConfigException.java    |  47 +++
 .../apache/bookkeeper/common/conf/ConfigKey.java   | 371 +++++++++++++++++++++
 .../bookkeeper/common/conf/ConfigKeyGroup.java     | 111 ++++++
 .../org/apache/bookkeeper/common/conf/Type.java    |  50 +++
 .../apache/bookkeeper/common/conf/Validator.java   |  48 +++
 .../common/conf/validators/ClassValidator.java     |  81 +++++
 .../common/conf/validators/NullValidator.java      |  46 +++
 .../common/conf/validators/RangeValidator.java     |  96 ++++++
 .../common/conf/validators/package-info.java       |  23 ++
 .../bookkeeper/common/conf/ConfigDefTest.java      | 300 +++++++++++++++++
 .../bookkeeper/common/conf/ConfigKeyGroupTest.java |  59 ++++
 .../bookkeeper/common/conf/ConfigKeyTest.java      | 336 +++++++++++++++++++
 .../common/conf/validators/ClassValidatorTest.java |  61 ++++
 .../common/conf/validators/RangeValidatorTest.java |  58 ++++
 .../src/test/resources/test_conf_2.conf            | 130 ++++++++
 .../bookkeeper/conf/ServerConfiguration.java       |  72 +++-
 .../bookie/SortedLedgerStorageCheckpointTest.java  |   2 +-
 .../bookkeeper/conf/TestServerConfiguration.java   |  41 +++
 pom.xml                                            |   3 +
 20 files changed, 2251 insertions(+), 11 deletions(-)

diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java
new file mode 100644
index 0000000..6e37ebc
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigDef.java
@@ -0,0 +1,327 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import com.google.common.collect.Sets;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * A definition of a configuration instance.
+ */
+@Slf4j
+@Getter
+public class ConfigDef {
+
+    /**
+     * Builder to build a configuration definition.
+     */
+    public static class Builder {
+
+        private final Set<ConfigKeyGroup> groups = new TreeSet<>(ConfigKeyGroup.ORDERING);
+        private final Map<String, Set<ConfigKey>> settings = new HashMap<>();
+
+        private Builder() {}
+
+        /**
+         * Add the config key group to the builder.
+         *
+         * @param group config key group
+         * @return builder to build this configuration def
+         */
+        public Builder withConfigKeyGroup(ConfigKeyGroup group) {
+            groups.add(group);
+            return this;
+        }
+
+        /**
+         * Add the config key to the builder.
+         *
+         * @param key the key to add to the builder.
+         * @return builder to build this configuration def
+         */
+        public Builder withConfigKey(ConfigKey key) {
+            ConfigKeyGroup group = key.group();
+            Set<ConfigKey> keys;
+            String groupName;
+            if (null == group) {
+                groupName = "";
+            } else {
+                groupName = group.name();
+                groups.add(group);
+            }
+            keys = settings.computeIfAbsent(groupName, name -> new TreeSet<>(ConfigKey.ORDERING));
+            keys.add(key);
+            return this;
+        }
+
+        public ConfigDef build() {
+            checkArgument(
+                Sets.difference(
+                    groups.stream().map(group -> group.name()).collect(Collectors.toSet()),
+                    settings.keySet()
+                ).isEmpty(),
+                "Configuration Key Groups doesn't match with keys");
+            return new ConfigDef(groups, settings);
+        }
+
+    }
+
+    /**
+     * Create a builder to build a config def.
+     *
+     * @return builder to build a config def.
+     */
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    private final Set<ConfigKeyGroup> groups;
+    private final Map<String, Set<ConfigKey>> settings;
+    private final Map<String, ConfigKey> keys;
+
+    private ConfigDef(Set<ConfigKeyGroup> groups,
+                      Map<String, Set<ConfigKey>> settings) {
+        this.groups = groups;
+        this.settings = settings;
+        this.keys = settings.values()
+            .stream()
+            .flatMap(keys -> keys.stream())
+            .collect(Collectors.toSet())
+            .stream()
+            .collect(Collectors.toMap(
+                key -> key.name(),
+                key -> key
+            ));
+    }
+
+    /**
+     * Validate if the provided <tt>conf</tt> is a valid configuration of this configuration definition.
+     *
+     * @param conf the configuration to validate
+     */
+    public void validate(Configuration conf) throws ConfigException {
+        for (ConfigKey key : keys.values()) {
+            key.validate(conf);
+        }
+    }
+
+    /**
+     * Build the config definitation of a config class.
+     *
+     * @param configClass config class
+     * @return config definition.
+     */
+    @SuppressWarnings("unchecked")
+    public static ConfigDef of(Class configClass) {
+        ConfigDef.Builder builder = ConfigDef.builder();
+
+        Field[] fields = configClass.getDeclaredFields();
+        for (Field field : fields) {
+            if (Modifier.isStatic(field.getModifiers()) && field.getType().equals(ConfigKey.class)) {
+                field.setAccessible(true);
+                try {
+                    builder.withConfigKey((ConfigKey) field.get(null));
+                } catch (IllegalAccessException e) {
+                    log.error("Illegal to access {}#{}", configClass.getSimpleName(), field.getName(), e);
+                }
+            }
+        }
+
+        return builder.build();
+    }
+
+    //
+    // Methods to save the configuration to an {@link OutputStream}
+    //
+
+    private static final int MAX_COLUMN_SIZE = 80;
+    private static final String COMMENT_PREFIX = "# ";
+
+    public void save(Path path) throws IOException  {
+        try (OutputStream stream = Files.newOutputStream(
+            path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) {
+            save(stream);
+        }
+    }
+
+    public void save(OutputStream os) throws IOException {
+        try (PrintStream ps = new PrintStream(os, false, UTF_8.name())) {
+            save(ps);
+            ps.flush();
+        }
+    }
+
+    private void writeNSharps(PrintStream stream, int num) {
+        IntStream.range(0, num).forEach(ignored -> stream.print("#"));
+    }
+
+    private void writeConfigKeyGroup(PrintStream stream, ConfigKeyGroup group) {
+        int maxLength = Math.min(
+            group.description().length() + COMMENT_PREFIX.length(),
+            MAX_COLUMN_SIZE
+        );
+        // "###########"
+        writeNSharps(stream, maxLength);
+        stream.println();
+        // "# Settings of `<group>`
+        writeSentence(stream, COMMENT_PREFIX, "Settings of `" + group.name() + "`");
+        stream.println("#");
+        // "# <group description>"
+        writeSentence(stream, COMMENT_PREFIX, group.description());
+        // "###########"
+        writeNSharps(stream, maxLength);
+        stream.println();
+    }
+
+    private void writeConfigKey(PrintStream stream,
+                                ConfigKey key) {
+        // "# <description>"
+        // "#"
+        if (StringUtils.isNotBlank(key.description())) {
+            writeSentence(stream, COMMENT_PREFIX, key.description());
+            stream.println("#");
+        }
+        // "# <documentation>"
+        // "#"
+        if (StringUtils.isNotBlank(key.documentation())) {
+            writeSentence(stream, COMMENT_PREFIX, key.documentation());
+            stream.println("#");
+        }
+        // "# type: <type>, required"
+        writeSentence(
+            stream,
+            COMMENT_PREFIX,
+            "TYPE: " + key.type() + ", " + (key.required() ? "required" : "optional"));
+        if (null != key.validator() && StringUtils.isNotBlank(key.validator().documentation())) {
+            writeSentence(
+                stream, COMMENT_PREFIX,
+                "@constraints : " + key.validator().documentation()
+            );
+        }
+        if (!key.optionValues().isEmpty()) {
+            writeSentence(
+                stream, COMMENT_PREFIX, "@options :"
+            );
+            key.optionValues().forEach(value -> {
+                writeSentence(
+                    stream, COMMENT_PREFIX, "  " + value
+                );
+            });
+        }
+        // "#"
+        // "# @Since"
+        if (StringUtils.isNotBlank(key.since())) {
+            stream.println("#");
+            writeSentence(stream, COMMENT_PREFIX,
+                "@since " + key.since() + "");
+        }
+        // "#"
+        // "# @Deprecated"
+        if (key.deprecated()) {
+            stream.println("#");
+            writeSentence(stream, COMMENT_PREFIX, getDeprecationDescription(key));
+        }
+        // <key>=<defaultValue>
+        stream.print(key.name());
+        stream.print("=");
+        if (null != key.defaultValue()) {
+            stream.print(key.defaultValue());
+        }
+        stream.println();
+    }
+
+    private String getDeprecationDescription(ConfigKey key) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("@deprecated");
+        if (StringUtils.isNotBlank(key.deprecatedSince())) {
+            sb.append(" since `")
+              .append(key.deprecatedSince())
+              .append("`");
+        }
+        if (StringUtils.isNotBlank(key.deprecatedByConfigKey())) {
+            sb.append(" in favor of using `")
+              .append(key.deprecatedByConfigKey())
+              .append("`");
+        }
+        return sb.toString();
+    }
+
+    private void writeSentence(PrintStream stream,
+                               String prefix,
+                               String sentence) {
+        int max = MAX_COLUMN_SIZE;
+        String[] words = sentence.split(" ");
+        int i = 0;
+        stream.print(prefix);
+        int current = prefix.length();
+        while (i < words.length) {
+            String word = words[i];
+            if (word.length() > max || current + word.length() <= max) {
+                if (i != 0) {
+                    stream.print(" ");
+                }
+                stream.print(word);
+                current += (word.length() + 1);
+            } else {
+                stream.println();
+                stream.print(prefix);
+                stream.print(word);
+                current = prefix.length() + word.length();
+            }
+            ++i;
+        }
+        stream.println();
+    }
+
+    private void save(PrintStream stream) {
+        for (ConfigKeyGroup group : groups) {
+            writeConfigKeyGroup(stream, group);
+            stream.println();
+            Set<ConfigKey> groupKeys = settings.getOrDefault(group.name(), Collections.emptySet());
+            groupKeys.forEach(key -> {
+                writeConfigKey(stream, key);
+                stream.println();
+            });
+        }
+    }
+
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java
new file mode 100644
index 0000000..a0534a6
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigException.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+/**
+ * Exception thrown for configuration errors.
+ */
+public class ConfigException extends Exception {
+
+    private static final long serialVersionUID = -7842276571881795108L;
+
+    /**
+     * Construct a config exception with provided error.
+     *
+     * @param error error message
+     */
+    public ConfigException(String error) {
+        super(error);
+    }
+
+    /**
+     * Construct a config exception with provided error and reason.
+     *
+     * @param error error message
+     * @param cause error cause
+     */
+    public ConfigException(String error, Throwable cause) {
+        super(error, cause);
+    }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java
new file mode 100644
index 0000000..b2bb47a
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKey.java
@@ -0,0 +1,371 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import lombok.Builder;
+import lombok.Builder.Default;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+import org.apache.bookkeeper.common.conf.validators.NullValidator;
+import org.apache.bookkeeper.common.util.ReflectionUtils;
+import org.apache.commons.configuration.Configuration;
+import org.apache.commons.configuration.ConfigurationException;
+
+/**
+ * A configuration key in a configuration.
+ */
+@Data
+@Builder(builderMethodName = "internalBuilder")
+@Accessors(fluent = true)
+@Public
+@Slf4j
+public class ConfigKey {
+
+    public static final Comparator<ConfigKey> ORDERING = (o1, o2) -> {
+        int ret = Integer.compare(o1.orderInGroup, o2.orderInGroup);
+        if (ret == 0) {
+            return o1.name().compareTo(o2.name());
+        } else {
+            return ret;
+        }
+    };
+
+    /**
+     * Build a config key of <tt>name</tt>.
+     *
+     * @param name config key name
+     * @return config key builder
+     */
+    public static ConfigKeyBuilder builder(String name) {
+        return internalBuilder().name(name);
+    }
+
+    /**
+     * Flag indicates whether the setting is required.
+     */
+    @Default
+    private boolean required = false;
+
+    /**
+     * Name of the configuration setting.
+     */
+    private String name;
+
+    /**
+     * Type of the configuration setting.
+     */
+    @Default
+    private Type type = Type.STRING;
+
+    /**
+     * Description of the configuration setting.
+     */
+    @Default
+    private String description = "";
+
+    /**
+     * Documentation of the configuration setting.
+     */
+    @Default
+    private String documentation = "";
+
+    /**
+     * Default value as a string representation.
+     */
+    @Default
+    private Object defaultValue = null;
+
+    private String defaultValueAsString() {
+        if (null == defaultValue) {
+            return null;
+        } else if (defaultValue instanceof String) {
+            return (String) defaultValue;
+        } else if (defaultValue instanceof Class) {
+            return ((Class) defaultValue).getName();
+        } else {
+            return defaultValue.toString();
+        }
+    }
+
+    /**
+     * The list of options for this setting.
+     */
+    @Default
+    private List<String> optionValues = Collections.emptyList();
+
+    /**
+     * The validator used for validating configuration value.
+     */
+    @Default
+    private Validator validator = NullValidator.of();
+
+    /**
+     * The key-group to group settings together.
+     */
+    @Default
+    private ConfigKeyGroup group = ConfigKeyGroup.DEFAULT;
+
+    /**
+     * The order of the setting in the key-group.
+     */
+    @Default
+    private int orderInGroup = Integer.MIN_VALUE;
+
+    /**
+     * The list of settings dependents on this setting.
+     */
+    @Default
+    private List<String> dependents = Collections.emptyList();
+
+    /**
+     * Whether this setting is deprecated or not.
+     */
+    @Default
+    private boolean deprecated = false;
+
+    /**
+     * The config key that deprecates this key.
+     */
+    @Default
+    private String deprecatedByConfigKey = "";
+
+    /**
+     * The version when this settings was deprecated.
+     */
+    @Default
+    private String deprecatedSince = "";
+
+    /**
+     * The version when this setting was introduced.
+     */
+    @Default
+    private String since = "";
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ConfigKey)) {
+            return false;
+        }
+        ConfigKey other = (ConfigKey) o;
+        return Objects.equals(name, other.name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return name.hashCode();
+    }
+
+    /**
+     * Validate the setting is valid in the provided config <tt>conf</tt>.
+     *
+     * @param conf configuration to test
+     */
+    public void validate(Configuration conf) throws ConfigException {
+        if (conf.containsKey(name()) && validator() != null) {
+            Object value = get(conf);
+            if (!validator().validate(name(), value)) {
+                throw new ConfigException("Invalid setting of '" + name()
+                    + "' found the configuration: value = '" + value + "', requirement = '" + validator + "'");
+            }
+        } else if (required()) { // missing config on a required field
+            throw new ConfigException(
+                "Setting '" + name() + "' is required but missing in the configuration");
+        }
+    }
+
+    /**
+     * Update the setting <tt>name</tt> in the configuration <tt>conf</tt> with the provided <tt>value</tt>.
+     *
+     * @param conf configuration to set
+     * @param value value of the setting
+     */
+    public void set(Configuration conf, Object value) {
+        if (!type().validator().validate(name(), value)) {
+            throw new IllegalArgumentException(
+                "Invalid value '" + value + "' to set on setting '" + name() + "': expected type = " + type);
+        }
+
+        if (null != validator() && !validator().validate(name(), value)) {
+            throw new IllegalArgumentException(
+                "Invalid value '" + value + "' to set on setting '" + name() + "': required '" + validator() + "'");
+        }
+
+        if (value instanceof Class) {
+            conf.setProperty(name(), ((Class) value).getName());
+        } else {
+            conf.setProperty(name(), value);
+        }
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Long} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a long number
+     */
+    public long getLong(Configuration conf) {
+        checkArgument(type() == Type.LONG, "'" + name() + "' is NOT a LONG numeric setting");
+        return conf.getLong(name(), (Long) defaultValue());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Integer} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as an integer number
+     */
+    public int getInt(Configuration conf) {
+        checkArgument(type() == Type.INT, "'" + name() + "' is NOT a INT numeric setting");
+        return conf.getInt(name(), (Integer) defaultValue());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Short} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a short number
+     */
+    public short getShort(Configuration conf) {
+        checkArgument(type() == Type.SHORT, "'" + name() + "' is NOT a SHORT numeric setting");
+        return conf.getShort(name(), (Short) defaultValue());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Boolean} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a boolean flag
+     */
+    public boolean getBoolean(Configuration conf) {
+        checkArgument(type() == Type.BOOLEAN, "'" + name() + "' is NOT a BOOL numeric setting");
+        return conf.getBoolean(name(), (Boolean) defaultValue());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Double} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a double number
+     */
+    public double getDouble(Configuration conf) {
+        checkArgument(type() == Type.DOUBLE, "'" + name() + "' is NOT a DOUBLE numeric setting");
+        return conf.getDouble(name(), (Double) defaultValue());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link String} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a string.
+     */
+    public String getString(Configuration conf) {
+        return conf.getString(name(), defaultValueAsString());
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a class
+     */
+    @SuppressWarnings("unchecked")
+    public <T> Class<? extends T> getClass(Configuration conf, Class<T> interfaceCls) {
+        checkArgument(type() == Type.CLASS, "'" + name() + "' is NOT a CLASS setting");
+        try {
+            Class<? extends T> defaultClass = (Class<? extends T>) defaultValue();
+            return ReflectionUtils.getClass(conf, name(), defaultClass, interfaceCls, getClass().getClassLoader());
+        } catch (ConfigurationException e) {
+            throw new IllegalArgumentException("Invalid class is set to setting '" + name() + "': ", e);
+        }
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as a class
+     */
+    @SuppressWarnings("unchecked")
+    public Class<?> getClass(Configuration conf) {
+        checkArgument(type() == Type.CLASS, "'" + name() + "' is NOT a CLASS setting");
+        try {
+            Class<?> defaultClass = (Class<?>) defaultValue();
+            return ReflectionUtils.getClass(conf, name(), defaultClass, getClass().getClassLoader());
+        } catch (ConfigurationException e) {
+            throw new IllegalArgumentException("Invalid class is set to setting '" + name() + "': ", e);
+        }
+    }
+
+    /**
+     * Retrieve the setting from the configuration <tt>conf</tt> as a {@link Class} value.
+     *
+     * @param conf configuration to retrieve the setting
+     * @return the value as list of values
+     */
+    @SuppressWarnings("unchecked")
+    public List<Object> getList(Configuration conf) {
+        checkArgument(type() == Type.LIST, "'" + name() + "' is NOT a LIST setting");
+        List<Object> list = (List<Object>) defaultValue();
+        if (null == list) {
+            list = Collections.emptyList();
+        }
+        return conf.getList(name(), list);
+    }
+
+    /**
+     * Retrieve the setting value from the provided <tt>conf</tt>.
+     *
+     * @return the setting value
+     */
+    public Object get(Configuration conf) {
+        switch (type()) {
+            case LONG:
+                return getLong(conf);
+            case INT:
+                return getInt(conf);
+            case SHORT:
+                return getShort(conf);
+            case DOUBLE:
+                return getDouble(conf);
+            case BOOLEAN:
+                return getBoolean(conf);
+            case LIST:
+                return getList(conf);
+            case CLASS:
+                return getClass(conf);
+            default:
+                return getString(conf);
+        }
+    }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java
new file mode 100644
index 0000000..833e907
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/ConfigKeyGroup.java
@@ -0,0 +1,111 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import lombok.Builder;
+import lombok.Builder.Default;
+import lombok.Data;
+import lombok.experimental.Accessors;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Define a group of configuration settings.
+ */
+@Data
+@Accessors(fluent = true)
+@Builder(builderMethodName = "internalBuilder")
+@Public
+public class ConfigKeyGroup {
+
+    /**
+     * Ordering the key groups in a configuration.
+     */
+    public static final Comparator<ConfigKeyGroup> ORDERING = (o1, o2) -> {
+        int ret = Integer.compare(o1.order, o2.order);
+        if (0 == ret) {
+            return o1.name().compareTo(o2.name());
+        } else {
+            return ret;
+        }
+    };
+
+    /**
+     * Create a config key group of <tt>name</tt>.
+     *
+     * @param name key group name
+     * @return key group builder
+     */
+    public static ConfigKeyGroupBuilder builder(String name) {
+        return internalBuilder().name(name);
+    }
+
+    /**
+     * The default key group.
+     */
+    public static final ConfigKeyGroup DEFAULT = builder("").build();
+
+    /**
+     * Name of the key group.
+     */
+    private String name;
+
+    /**
+     * Description of the key group.
+     */
+    @Default
+    private String description = "";
+
+    /**
+     * The list of sub key-groups of this key group.
+     */
+    @Default
+    private List<String> children = Collections.emptyList();
+
+    /**
+     * The order of the key-group in a configuration.
+     */
+    @Default
+    private int order = Integer.MIN_VALUE;
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ConfigKeyGroup)) {
+            return false;
+        }
+        ConfigKeyGroup other = (ConfigKeyGroup) o;
+        return Objects.equals(name, other.name);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int hashCode() {
+        return name.hashCode();
+    }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java
new file mode 100644
index 0000000..c48e94c
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Type.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import java.util.List;
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Config key types.
+ */
+@Public
+public enum Type {
+
+    BOOLEAN((name, value) -> value instanceof Boolean),
+    STRING((name, value) -> value instanceof String),
+    INT((name, value) -> value instanceof Integer),
+    SHORT((name, value) -> value instanceof Short),
+    LONG((name, value) -> value instanceof Long),
+    DOUBLE((name, value) -> value instanceof Double),
+    LIST((name, value) -> value instanceof List),
+    CLASS((name, value) -> value instanceof Class || value instanceof String);
+
+    private Validator validator;
+
+    Type(Validator validator) {
+        this.validator = validator;
+    }
+
+    public Validator validator() {
+        return validator;
+    }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java
new file mode 100644
index 0000000..249ad31
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/Validator.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import org.apache.bookkeeper.common.annotation.InterfaceAudience.Public;
+
+/**
+ * Validator that validates configuration settings.
+ */
+@Public
+public interface Validator {
+
+    /**
+     * Validates the configuration value.
+     *
+     * @param name name of the configuration setting
+     * @param value value of the configuration setting
+     * @return true if it is a valid value, otherwise false.
+     */
+    boolean validate(String name, Object value);
+
+    /**
+     * Return the documentation for a given validator.
+     *
+     * @return the documentation for a given validator
+     */
+    default String documentation() {
+        return "";
+    }
+
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java
new file mode 100644
index 0000000..dcd5f41
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/ClassValidator.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf.validators;
+
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.conf.Validator;
+import org.apache.bookkeeper.common.util.ReflectionUtils;
+
+/**
+ * Validator that validates a configuration setting is returning a given type of class.
+ */
+@Slf4j
+@Data
+public class ClassValidator<T> implements Validator {
+
+    /**
+     * Create a validator to validate if a setting is returning a class that extends from
+     * <tt>interfaceClass</tt>.
+     *
+     * @param interfaceClass interface class
+     * @return the validator that expects a setting return a class that extends from <tt>interfaceClass</tt>
+     */
+    public static <T> ClassValidator<T> of(Class<T> interfaceClass) {
+        return new ClassValidator<>(interfaceClass);
+    }
+
+    private final Class<T> interfaceClass;
+
+    @Override
+    public boolean validate(String name, Object value) {
+        if (value instanceof String) {
+            try {
+                ReflectionUtils.forName((String) value, interfaceClass);
+                return true;
+            } catch (RuntimeException re) {
+                log.warn("Setting value of '{}' is not '{}' : {}",
+                    name, interfaceClass.getName(), value, re);
+                return false;
+            }
+        } else if (value instanceof Class) {
+            Class cls = (Class) value;
+            if (!interfaceClass.isAssignableFrom(cls)) {
+                log.warn("Setting value of '{}' is not '{}' : {}",
+                    name, interfaceClass.getName(), cls.getName());
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Class extends " + interfaceClass.getName();
+    }
+
+    @Override
+    public String documentation() {
+        return "class extends `" + interfaceClass.getName() + "`";
+    }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java
new file mode 100644
index 0000000..1d384df
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/NullValidator.java
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf.validators;
+
+import org.apache.bookkeeper.common.conf.Validator;
+
+/**
+ * A validator that does nothing.
+ */
+public class NullValidator implements Validator {
+
+    /**
+     * Return the instance of NullValidator.
+     *
+     * @return the instance of NullValidator.
+     */
+    public static NullValidator of() {
+        return INSTANCE;
+    }
+
+    private static final NullValidator INSTANCE = new NullValidator();
+
+    private NullValidator() {}
+
+    @Override
+    public boolean validate(String name, Object value) {
+        return true;
+    }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java
new file mode 100644
index 0000000..2dbadf4
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/RangeValidator.java
@@ -0,0 +1,96 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf.validators;
+
+import lombok.Data;
+import org.apache.bookkeeper.common.conf.Validator;
+
+/**
+ * Validator that validates a configuration value is in a numeric range.
+ */
+@Data
+public class RangeValidator implements Validator {
+
+    /**
+     * A numeric range that checks the lower bound.
+     *
+     * @param min the minimum acceptable value
+     * @return a numeric range that checks the lower bound
+     */
+    public static RangeValidator atLeast(Number min) {
+        return new RangeValidator(min, null);
+    }
+
+    /**
+     * A numeric range that checks the upper bound.
+     *
+     * @param max the maximum acceptable value
+     * @return a numeric range that checks the upper bound
+     */
+    public static RangeValidator atMost(Number max) {
+        return new RangeValidator(null, max);
+    }
+
+    /**
+     * A numeric range that checks both lower and upper bounds.
+     *
+     * @param min the minimum acceptable value
+     * @param max the maximum acceptable value
+     * @return a numeric range that checks both lower and upper bounds
+     */
+    public static RangeValidator between(Number min, Number max) {
+        return new RangeValidator(min, max);
+    }
+
+    private final Number min;
+    private final Number max;
+
+    @Override
+    public boolean validate(String name, Object value) {
+        if (value instanceof Number) {
+            Number n = (Number) value;
+            if (min != null && n.doubleValue() < min.doubleValue()) {
+                return false;
+            } else if (max != null && n.doubleValue() > max.doubleValue()) {
+                return false;
+            } else {
+                return true;
+            }
+        } else {
+            return false;
+        }
+    }
+
+    @Override
+    public String toString() {
+        if (null == min) {
+            return "[... , " + max + "]";
+        } else if (null == max) {
+            return "[" + min + ", ...]";
+        } else {
+            return "[" + min + ", " + max + "]";
+        }
+    }
+
+    @Override
+    public String documentation() {
+        return toString();
+    }
+}
diff --git a/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java
new file mode 100644
index 0000000..e4c141a
--- /dev/null
+++ b/bookkeeper-common/src/main/java/org/apache/bookkeeper/common/conf/validators/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A collection of validators that validate configuration settings.
+ */
+package org.apache.bookkeeper.common.conf.validators;
\ No newline at end of file
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java
new file mode 100644
index 0000000..7ba3e71
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigDefTest.java
@@ -0,0 +1,300 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.Set;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.bookkeeper.common.conf.validators.ClassValidator;
+import org.apache.bookkeeper.common.conf.validators.RangeValidator;
+import org.junit.Test;
+
+/**
+ * Unit test {@link ConfigDef}.
+ */
+@Slf4j
+public class ConfigDefTest {
+
+    private static class TestConfig {
+
+        private static final ConfigKeyGroup group1 = ConfigKeyGroup.builder("group1")
+            .description("Group 1 Settings")
+            .order(1)
+            .build();
+
+        private static final ConfigKey key11 = ConfigKey.builder("key11")
+            .type(Type.LONG)
+            .group(group1)
+            .validator(RangeValidator.atLeast(1000))
+            .build();
+
+        private static final ConfigKeyGroup group2 = ConfigKeyGroup.builder("group2")
+            .description("Group 2 Settings")
+            .order(2)
+            .build();
+
+        private static final ConfigKey key21 = ConfigKey.builder("key21")
+            .type(Type.LONG)
+            .group(group2)
+            .validator(RangeValidator.atMost(1000))
+            .orderInGroup(2)
+            .build();
+
+        private static final ConfigKey key22 = ConfigKey.builder("key22")
+            .type(Type.STRING)
+            .group(group2)
+            .validator(ClassValidator.of(Runnable.class))
+            .orderInGroup(1)
+            .build();
+
+    }
+
+    private static class TestConfig2 {
+
+        private static final ConfigKeyGroup emptyGroup = ConfigKeyGroup.builder("empty_group")
+            .description("Empty Group Settings")
+            .order(1)
+            .build();
+
+        private static final ConfigKeyGroup group1 = ConfigKeyGroup.builder("group1")
+            .description("This is a very long description : Lorem ipsum dolor sit amet,"
+                + " consectetur adipiscing elit. Maecenas bibendum ac felis id commodo."
+                + " Etiam mauris purus, fringilla id tempus in, mollis vel orci. Duis"
+                + " ultricies at erat eget iaculis.")
+            .order(2)
+            .build();
+
+        private static final ConfigKey intKey = ConfigKey.builder("int_key")
+            .type(Type.INT)
+            .description("it is an int key")
+            .group(group1)
+            .validator(RangeValidator.atLeast(1000))
+            .build();
+
+        private static final ConfigKey longKey = ConfigKey.builder("long_key")
+            .type(Type.LONG)
+            .description("it is a long key")
+            .group(group1)
+            .validator(RangeValidator.atMost(1000))
+            .build();
+
+        private static final ConfigKey shortKey = ConfigKey.builder("short_key")
+            .type(Type.SHORT)
+            .description("it is a short key")
+            .group(group1)
+            .validator(RangeValidator.between(500, 1000))
+            .build();
+
+        private static final ConfigKey doubleKey = ConfigKey.builder("double_key")
+            .type(Type.DOUBLE)
+            .description("it is a double key")
+            .group(group1)
+            .validator(RangeValidator.between(1234.0f, 5678.0f))
+            .build();
+
+        private static final ConfigKey boolKey = ConfigKey.builder("bool_key")
+            .type(Type.BOOLEAN)
+            .description("it is a bool key")
+            .group(group1)
+            .build();
+
+        private static final ConfigKey classKey = ConfigKey.builder("class_key")
+            .type(Type.CLASS)
+            .description("it is a class key")
+            .validator(ClassValidator.of(Runnable.class))
+            .group(group1)
+            .build();
+
+        private static final ConfigKey listKey = ConfigKey.builder("list_key")
+            .type(Type.LIST)
+            .description("it is a list key")
+            .group(group1)
+            .build();
+
+        private static final ConfigKey stringKey = ConfigKey.builder("string_key")
+            .type(Type.STRING)
+            .description("it is a string key")
+            .group(group1)
+            .build();
+
+        private static final ConfigKeyGroup group2 = ConfigKeyGroup.builder("group2")
+            .description("This group has short description")
+            .order(3)
+            .build();
+
+        private static final ConfigKey keyWithSince = ConfigKey.builder("key_with_since")
+            .type(Type.STRING)
+            .description("it is a string key with since")
+            .since("4.7.0")
+            .group(group2)
+            .orderInGroup(10)
+            .build();
+
+        private static final ConfigKey keyWithDocumentation = ConfigKey.builder("key_with_short_documentation")
+            .type(Type.STRING)
+            .description("it is a string key with documentation")
+            .documentation("it has a short documentation")
+            .group(group2)
+            .orderInGroup(9)
+            .build();
+
+        private static final ConfigKey keyWithLongDocumentation =
+            ConfigKey.builder("key_long_short_documentation")
+                .type(Type.STRING)
+                .description("it is a string key with documentation")
+                .documentation("it has a long documentation : Lorem ipsum dolor sit amet,"
+                    + " consectetur adipiscing elit. Maecenas bibendum ac felis id commodo."
+                    + " Etiam mauris purus, fringilla id tempus in, mollis vel orci. Duis"
+                    + " ultricies at erat eget iaculis.")
+                .group(group2)
+                .orderInGroup(8)
+                .build();
+
+        private static final ConfigKey keyWithDefaultValue = ConfigKey.builder("key_with_default_value")
+            .type(Type.STRING)
+            .description("it is a string key with default value")
+            .defaultValue("this-is-a-test-value")
+            .group(group2)
+            .orderInGroup(7)
+            .build();
+
+        private static final ConfigKey keyWithOptionalValues = ConfigKey.builder("key_with_optional_values")
+            .type(Type.STRING)
+            .description("it is a string key with optional values")
+            .defaultValue("this-is-a-default-value")
+            .optionValues(Lists.newArrayList(
+                "item1", "item2", "item3", "item3"
+            ))
+            .group(group2)
+            .orderInGroup(6)
+            .build();
+
+        private static final ConfigKey deprecatedKey = ConfigKey.builder("deprecated_key")
+            .type(Type.STRING)
+            .deprecated(true)
+            .description("it is a deprecated key")
+            .group(group2)
+            .orderInGroup(5)
+            .build();
+
+        private static final ConfigKey deprecatedKeyWithSince = ConfigKey.builder("deprecated_key_with_since")
+            .type(Type.STRING)
+            .deprecated(true)
+            .deprecatedSince("4.3.0")
+            .description("it is a deprecated key with since")
+            .group(group2)
+            .orderInGroup(4)
+            .build();
+
+        private static final ConfigKey deprecatedKeyWithReplacedKey =
+            ConfigKey.builder("deprecated_key_with_replaced_key")
+                .type(Type.STRING)
+                .deprecated(true)
+                .deprecatedByConfigKey("key_with_optional_values")
+                .description("it is a deprecated key with replaced key")
+                .group(group2)
+                .orderInGroup(3)
+                .build();
+
+        private static final ConfigKey deprecatedKeyWithSinceAndReplacedKey =
+            ConfigKey.builder("deprecated_key_with_since_and_replaced_key")
+                .type(Type.STRING)
+                .deprecated(true)
+                .deprecatedSince("4.3.0")
+                .deprecatedByConfigKey("key_with_optional_values")
+                .description("it is a deprecated key with since and replaced key")
+                .group(group2)
+                .orderInGroup(2)
+                .build();
+
+        private static final ConfigKey requiredKey = ConfigKey.builder("required_key")
+            .type(Type.STRING)
+            .required(true)
+            .description("it is a required key")
+            .group(group2)
+            .orderInGroup(1)
+            .build();
+
+    }
+
+    @Test
+    public void testBuildConfigDef() {
+        ConfigDef configDef = ConfigDef.of(TestConfig.class);
+        assertEquals(2, configDef.getGroups().size());
+
+        Iterator<ConfigKeyGroup> grpIter = configDef.getGroups().iterator();
+
+        // iterate over group 1
+        assertTrue(grpIter.hasNext());
+        ConfigKeyGroup group1 = grpIter.next();
+        assertSame(TestConfig.group1, group1);
+        Set<ConfigKey> keys = configDef.getSettings().get(group1.name());
+        assertNotNull(keys);
+        assertEquals(1, keys.size());
+        assertEquals(TestConfig.key11, keys.iterator().next());
+
+        // iterate over group 2
+        assertTrue(grpIter.hasNext());
+        ConfigKeyGroup group2 = grpIter.next();
+        assertSame(TestConfig.group2, group2);
+        keys = configDef.getSettings().get(group2.name());
+        assertNotNull(keys);
+        assertEquals(2, keys.size());
+        Iterator<ConfigKey> keyIter = keys.iterator();
+        assertEquals(TestConfig.key22, keyIter.next());
+        assertEquals(TestConfig.key21, keyIter.next());
+        assertFalse(keyIter.hasNext());
+
+        // no more group
+        assertFalse(grpIter.hasNext());
+    }
+
+    @Test
+    public void testSaveConfigDef() throws IOException  {
+        byte[] confData;
+        try (InputStream is = this.getClass().getClassLoader().getResourceAsStream("test_conf_2.conf")) {
+            confData = new byte[is.available()];
+            ByteStreams.readFully(is, confData);
+        }
+
+        ConfigDef configDef = ConfigDef.of(TestConfig2.class);
+        String readConf;
+        try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
+            configDef.save(baos);
+            readConf = baos.toString();
+            log.info("\n{}", readConf);
+        }
+
+        assertEquals(new String(confData, UTF_8), readConf);
+    }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java
new file mode 100644
index 0000000..a8abefa
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyGroupTest.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.bookkeeper.common.conf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link ConfigKeyGroup}.
+ */
+public class ConfigKeyGroupTest {
+
+    @Test
+    public void testEquals() {
+        ConfigKeyGroup grp1 = ConfigKeyGroup.builder("group1")
+            .description("test group 1")
+            .build();
+        ConfigKeyGroup anotherGrp1 = ConfigKeyGroup.builder("group1")
+            .description("test another group 1")
+            .build();
+
+        assertEquals(grp1, anotherGrp1);
+    }
+
+    @Test
+    public void testOrdering() {
+        ConfigKeyGroup grp10 = ConfigKeyGroup.builder("group1")
+            .order(0)
+            .build();
+        ConfigKeyGroup grp20 = ConfigKeyGroup.builder("group2")
+            .order(0)
+            .build();
+        ConfigKeyGroup grp21 = ConfigKeyGroup.builder("group2")
+            .order(1)
+            .build();
+
+        assertTrue(ConfigKeyGroup.ORDERING.compare(grp10, grp20) < 0);
+        assertTrue(ConfigKeyGroup.ORDERING.compare(grp20, grp21) < 0);
+    }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java
new file mode 100644
index 0000000..858a615
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/ConfigKeyTest.java
@@ -0,0 +1,336 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.Function;
+import org.apache.commons.configuration.CompositeConfiguration;
+import org.apache.commons.configuration.Configuration;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestName;
+
+/**
+ * Unit test {@link ConfigKey}.
+ */
+public class ConfigKeyTest {
+
+    /**
+     * Test Function A.
+     */
+    private static class TestFunctionA implements Function<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return s + "!";
+        }
+    }
+
+    /**
+     * Test Function B.
+     */
+    private static class TestFunctionB implements Function<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return s + "!";
+        }
+    }
+
+    /**
+     * Test Function C.
+     */
+    private static class TestFunctionC implements Function<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return s + "!";
+        }
+    }
+
+    @Rule
+    public final TestName runtime = new TestName();
+
+    @Test
+    public void testValidateRequiredField() {
+        String keyName = runtime.getMethodName();
+        Configuration conf = new ConcurrentConfiguration();
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .build();
+
+        try {
+            key.validate(conf);
+            fail("Required key should exist in the configuration");
+        } catch (ConfigException ce) {
+            // expected
+        }
+    }
+
+    @Test
+    public void testValidateFieldSuccess() throws ConfigException {
+        String keyName = runtime.getMethodName();
+        Validator validator = mock(Validator.class);
+        when(validator.validate(anyString(), any())).thenReturn(true);
+        Configuration conf = new ConcurrentConfiguration();
+        conf.setProperty(keyName, "test-value");
+        ConfigKey key = ConfigKey.builder(keyName)
+            .validator(validator)
+            .build();
+
+        key.validate(conf);
+        verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
+    }
+
+    @Test
+    public void testValidateFieldFailure() {
+        String keyName = runtime.getMethodName();
+        Validator validator = mock(Validator.class);
+        when(validator.validate(anyString(), any())).thenReturn(false);
+        Configuration conf = new ConcurrentConfiguration();
+        conf.setProperty(keyName, "test-value");
+        ConfigKey key = ConfigKey.builder(keyName)
+            .validator(validator)
+            .build();
+
+        try {
+            key.validate(conf);
+            fail("Should fail validation if validator#validate returns false");
+        } catch (ConfigException ce) {
+            // expected
+        }
+        verify(validator, times(1)).validate(eq(keyName), eq("test-value"));
+    }
+
+    @Test
+    public void testGetLong() {
+        String keyName = runtime.getMethodName();
+        long defaultValue = System.currentTimeMillis();
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.LONG)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new ConcurrentConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getLong(conf));
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        long newValue = System.currentTimeMillis() * 2;
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getLong(conf));
+        assertEquals(newValue, key.get(conf));
+    }
+
+    @Test
+    public void testGetInt() {
+        String keyName = runtime.getMethodName();
+        int defaultValue = ThreadLocalRandom.current().nextInt(10000);
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.INT)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new ConcurrentConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getInt(conf));
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        int newValue = defaultValue * 2;
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getInt(conf));
+        assertEquals(newValue, key.get(conf));
+    }
+
+    @Test
+    public void testGetShort() {
+        String keyName = runtime.getMethodName();
+        short defaultValue = (short) ThreadLocalRandom.current().nextInt(10000);
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.SHORT)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new ConcurrentConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getShort(conf));
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        short newValue = (short) (defaultValue * 2);
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getShort(conf));
+        assertEquals(newValue, key.get(conf));
+    }
+
+    @Test
+    public void testGetDouble() {
+        String keyName = runtime.getMethodName();
+        double defaultValue = ThreadLocalRandom.current().nextDouble(10000.0f);
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.DOUBLE)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new ConcurrentConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getDouble(conf), 0.0001);
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        double newValue = (defaultValue * 2);
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getDouble(conf), 0.0001);
+        assertEquals(newValue, key.get(conf));
+    }
+
+    @Test
+    public void testGetBoolean() {
+        String keyName = runtime.getMethodName();
+        boolean defaultValue = ThreadLocalRandom.current().nextBoolean();
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.BOOLEAN)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new ConcurrentConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getBoolean(conf));
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        boolean newValue = !defaultValue;
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getBoolean(conf));
+        assertEquals(newValue, key.get(conf));
+    }
+
+    @Test
+    public void testGetList() {
+        String keyName = runtime.getMethodName();
+        List<String> defaultList = Lists.newArrayList(
+            "item1", "item2", "item3"
+        );
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.LIST)
+            .defaultValue(defaultList)
+            .build();
+
+        Configuration conf = new CompositeConfiguration();
+
+        // get default value
+        assertEquals(defaultList, key.getList(conf));
+        assertEquals(defaultList, key.get(conf));
+
+        // set value
+        List<String> newList = Lists.newArrayList(
+            "item4", "item5", "item6"
+        );
+        key.set(conf, newList);
+        assertEquals(newList, key.getList(conf));
+        assertEquals(newList, key.get(conf));
+
+        // set string value
+        newList = Lists.newArrayList(
+            "item7", "item8", "item9"
+        );
+        conf.setProperty(key.name(), "item7,item8,item9");
+        assertEquals(newList, key.getList(conf));
+        assertEquals(newList, key.get(conf));
+    }
+
+    @Test
+    public void testGetClass() {
+        String keyName = runtime.getMethodName();
+        Class defaultClass = TestFunctionA.class;
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.CLASS)
+            .defaultValue(defaultClass)
+            .build();
+
+        Configuration conf = new CompositeConfiguration();
+
+        // get default value
+        assertEquals(defaultClass, key.getClass(conf));
+        assertEquals(defaultClass, key.get(conf));
+
+        // set value
+        Class newClass = TestFunctionB.class;
+        key.set(conf, newClass);
+        assertEquals(newClass, key.getClass(conf));
+        assertEquals(newClass, key.get(conf));
+
+        // set string value
+        String newClassName = TestFunctionC.class.getName();
+        conf.setProperty(key.name(), newClassName);
+        assertEquals(TestFunctionC.class, key.getClass(conf));
+        assertEquals(TestFunctionC.class, key.get(conf));
+    }
+
+    @Test
+    public void testGetString() {
+        String keyName = runtime.getMethodName();
+        String defaultValue = "default-string-value";
+        ConfigKey key = ConfigKey.builder(keyName)
+            .required(true)
+            .type(Type.STRING)
+            .defaultValue(defaultValue)
+            .build();
+
+        Configuration conf = new CompositeConfiguration();
+
+        // get default value
+        assertEquals(defaultValue, key.getString(conf));
+        assertEquals(defaultValue, key.get(conf));
+
+        // set value
+        String newValue = "new-string-value";
+        key.set(conf, newValue);
+        assertEquals(newValue, key.getString(conf));
+        assertEquals(newValue, key.get(conf));
+    }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java
new file mode 100644
index 0000000..bfb7971
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/ClassValidatorTest.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf.validators;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.function.Function;
+import org.junit.Test;
+
+/**
+ * Unit test for {@link ClassValidator}.
+ */
+public class ClassValidatorTest {
+
+    private static class TestFunction implements Function<String, String> {
+
+        @Override
+        public String apply(String s) {
+            return s + "!";
+        }
+    }
+
+    @Test
+    public void testValidateStrings() {
+        ClassValidator<Function> validator = ClassValidator.of(Function.class);
+        assertTrue(validator.validate("test-valid-classname", TestFunction.class.getName()));
+        assertFalse(validator.validate("test-invalid-classname", "unknown"));
+    }
+
+    @Test
+    public void testValidateClass() {
+        ClassValidator<Function> validator = ClassValidator.of(Function.class);
+        assertTrue(validator.validate("test-valid-class", TestFunction.class));
+        assertFalse(validator.validate("test-invalid-class", Integer.class));
+    }
+
+    @Test
+    public void testValidateWrongType() {
+        ClassValidator<Function> validator = ClassValidator.of(Function.class);
+        assertFalse(validator.validate("test-invalid-type", 12345));
+    }
+
+}
diff --git a/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java
new file mode 100644
index 0000000..b872595
--- /dev/null
+++ b/bookkeeper-common/src/test/java/org/apache/bookkeeper/common/conf/validators/RangeValidatorTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.bookkeeper.common.conf.validators;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+
+/**
+ * Unit test {@link RangeValidator} validator.
+ */
+public class RangeValidatorTest {
+
+    @Test
+    public void testAtLeastRangeValidator() {
+        RangeValidator range = RangeValidator.atLeast(1234L);
+        assertTrue(range.validate("test-0", 1235L));
+        assertTrue(range.validate("test-1", 1234L));
+        assertFalse(range.validate("test-2", 1233L));
+    }
+
+    @Test
+    public void testAtMostRangeValidator() {
+        RangeValidator range = RangeValidator.atMost(1234L);
+        assertFalse(range.validate("test-0", 1235L));
+        assertTrue(range.validate("test-1", 1234L));
+        assertTrue(range.validate("test-2", 1233L));
+    }
+
+    @Test
+    public void testBetweenRangeValidator() {
+        RangeValidator range = RangeValidator.between(1230L, 1240L);
+        assertTrue(range.validate("test-0", 1230L));
+        assertTrue(range.validate("test-1", 1235L));
+        assertTrue(range.validate("test-2", 1240L));
+        assertFalse(range.validate("test-3", 1229L));
+        assertFalse(range.validate("test-4", 1241L));
+    }
+
+}
diff --git a/bookkeeper-common/src/test/resources/test_conf_2.conf b/bookkeeper-common/src/test/resources/test_conf_2.conf
new file mode 100644
index 0000000..ca6f7bb
--- /dev/null
+++ b/bookkeeper-common/src/test/resources/test_conf_2.conf
@@ -0,0 +1,130 @@
+################################################################################
+# Settings of `group1`
+#
+# This is a very long description : Lorem ipsum dolor sit amet, consectetur
+# adipiscing elit. Maecenas bibendum ac felis id commodo. Etiam mauris purus,
+# fringilla id tempus in, mollis vel orci. Duis ultricies at erat eget iaculis.
+################################################################################
+
+# it is a bool key
+#
+# TYPE: BOOLEAN, optional
+bool_key=
+
+# it is a class key
+#
+# TYPE: CLASS, optional
+# @constraints : class extends `java.lang.Runnable`
+class_key=
+
+# it is a double key
+#
+# TYPE: DOUBLE, optional
+# @constraints : [1234.0, 5678.0]
+double_key=
+
+# it is an int key
+#
+# TYPE: INT, optional
+# @constraints : [1000, ...]
+int_key=
+
+# it is a list key
+#
+# TYPE: LIST, optional
+list_key=
+
+# it is a long key
+#
+# TYPE: LONG, optional
+# @constraints : [... , 1000]
+long_key=
+
+# it is a short key
+#
+# TYPE: SHORT, optional
+# @constraints : [500, 1000]
+short_key=
+
+# it is a string key
+#
+# TYPE: STRING, optional
+string_key=
+
+##################################
+# Settings of `group2`
+#
+# This group has short description
+##################################
+
+# it is a required key
+#
+# TYPE: STRING, required
+required_key=
+
+# it is a deprecated key with since and replaced key
+#
+# TYPE: STRING, optional
+#
+# @deprecated since `4.3.0` in favor of using `key_with_optional_values`
+deprecated_key_with_since_and_replaced_key=
+
+# it is a deprecated key with replaced key
+#
+# TYPE: STRING, optional
+#
+# @deprecated in favor of using `key_with_optional_values`
+deprecated_key_with_replaced_key=
+
+# it is a deprecated key with since
+#
+# TYPE: STRING, optional
+#
+# @deprecated since `4.3.0`
+deprecated_key_with_since=
+
+# it is a deprecated key
+#
+# TYPE: STRING, optional
+#
+# @deprecated
+deprecated_key=
+
+# it is a string key with optional values
+#
+# TYPE: STRING, optional
+# @options :
+#   item1
+#   item2
+#   item3
+#   item3
+key_with_optional_values=this-is-a-default-value
+
+# it is a string key with default value
+#
+# TYPE: STRING, optional
+key_with_default_value=this-is-a-test-value
+
+# it is a string key with documentation
+#
+# it has a long documentation : Lorem ipsum dolor sit amet, consectetur
+# adipiscing elit. Maecenas bibendum ac felis id commodo. Etiam mauris purus,
+# fringilla id tempus in, mollis vel orci. Duis ultricies at erat eget iaculis.
+#
+# TYPE: STRING, optional
+key_long_short_documentation=
+
+# it is a string key with documentation
+#
+# it has a short documentation
+#
+# TYPE: STRING, optional
+key_with_short_documentation=
+
+# it is a string key with since
+#
+# TYPE: STRING, optional
+#
+# @since 4.7.0
+key_with_since=
+
diff --git a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
index 2a77e91..1d38651 100644
--- a/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
+++ b/bookkeeper-server/src/main/java/org/apache/bookkeeper/conf/ServerConfiguration.java
@@ -17,28 +17,77 @@
  */
 package org.apache.bookkeeper.conf;
 
+import static org.apache.bookkeeper.util.BookKeeperConstants.MAX_LOG_SIZE_LIMIT;
+
 import com.google.common.annotations.Beta;
 import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
 import java.io.File;
 import java.util.concurrent.TimeUnit;
 import org.apache.bookkeeper.bookie.InterleavedLedgerStorage;
 import org.apache.bookkeeper.bookie.LedgerStorage;
 import org.apache.bookkeeper.bookie.SortedLedgerStorage;
+import org.apache.bookkeeper.bookie.storage.ldb.DbLedgerStorage;
+import org.apache.bookkeeper.common.conf.ConfigDef;
+import org.apache.bookkeeper.common.conf.ConfigException;
+import org.apache.bookkeeper.common.conf.ConfigKey;
+import org.apache.bookkeeper.common.conf.ConfigKeyGroup;
+import org.apache.bookkeeper.common.conf.Type;
+import org.apache.bookkeeper.common.conf.validators.ClassValidator;
+import org.apache.bookkeeper.common.conf.validators.RangeValidator;
 import org.apache.bookkeeper.common.util.ReflectionUtils;
 import org.apache.bookkeeper.discover.RegistrationManager;
 import org.apache.bookkeeper.discover.ZKRegistrationManager;
 import org.apache.bookkeeper.stats.NullStatsProvider;
 import org.apache.bookkeeper.stats.StatsProvider;
-import org.apache.bookkeeper.util.BookKeeperConstants;
 import org.apache.commons.configuration.ConfigurationException;
 
 /**
  * Configuration manages server-side settings.
  */
 public class ServerConfiguration extends AbstractConfiguration<ServerConfiguration> {
+
+    // Ledger Storage Settings
+
+    private static final ConfigKeyGroup GROUP_LEDGER_STORAGE = ConfigKeyGroup.builder("ledgerstorage")
+        .description("Ledger Storage related settings")
+        .order(10) // place a place holder here
+        .build();
+
+    protected static final String LEDGER_STORAGE_CLASS = "ledgerStorageClass";
+    protected static final ConfigKey LEDGER_STORAGE_CLASS_KEY = ConfigKey.builder(LEDGER_STORAGE_CLASS)
+        .type(Type.CLASS)
+        .description("Ledger storage implementation class")
+        .defaultValue(SortedLedgerStorage.class)
+        .optionValues(Lists.newArrayList(
+            InterleavedLedgerStorage.class.getName(),
+            SortedLedgerStorage.class.getName(),
+            DbLedgerStorage.class.getName()
+        ))
+        .validator(ClassValidator.of(LedgerStorage.class))
+        .group(GROUP_LEDGER_STORAGE)
+        .build();
+
     // Entry Log Parameters
+
+    private static final ConfigKeyGroup GROUP_LEDGER_STORAGE_ENTRY_LOGGER = ConfigKeyGroup.builder("entrylogger")
+        .description("EntryLogger related settings")
+        .order(11)
+        .build();
+
     protected static final String ENTRY_LOG_SIZE_LIMIT = "logSizeLimit";
+    protected static final ConfigKey ENTRY_LOG_SIZE_LIMIT_KEY = ConfigKey.builder(ENTRY_LOG_SIZE_LIMIT)
+        .type(Type.LONG)
+        .description("Max file size of entry logger, in bytes")
+        .documentation("A new entry log file will be created when the old one reaches this file size limitation")
+        .defaultValue(MAX_LOG_SIZE_LIMIT)
+        .validator(RangeValidator.between(0, MAX_LOG_SIZE_LIMIT))
+        .group(GROUP_LEDGER_STORAGE_ENTRY_LOGGER)
+        .build();
+
     protected static final String ENTRY_LOG_FILE_PREALLOCATION_ENABLED = "entryLogFilePreallocationEnabled";
+
+
     protected static final String MINOR_COMPACTION_INTERVAL = "minorCompactionInterval";
     protected static final String MINOR_COMPACTION_THRESHOLD = "minorCompactionThreshold";
     protected static final String MAJOR_COMPACTION_INTERVAL = "majorCompactionInterval";
@@ -166,7 +215,6 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
     protected static final String ENABLE_STATISTICS = "enableStatistics";
     protected static final String STATS_PROVIDER_CLASS = "statsProviderClass";
 
-    protected static final String LEDGER_STORAGE_CLASS = "ledgerStorageClass";
 
     // Rx adaptive ByteBuf allocator parameters
     protected static final String BYTEBUF_ALLOCATOR_SIZE_INITIAL = "byteBufAllocatorSizeInitial";
@@ -256,7 +304,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * @return entry logger size limitation
      */
     public long getEntryLogSizeLimit() {
-        return this.getLong(ENTRY_LOG_SIZE_LIMIT, 1 * 1024 * 1024 * 1024L);
+        return ENTRY_LOG_SIZE_LIMIT_KEY.getLong(this);
     }
 
     /**
@@ -266,7 +314,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      *          new log size limitation
      */
     public ServerConfiguration setEntryLogSizeLimit(long logSizeLimit) {
-        this.setProperty(ENTRY_LOG_SIZE_LIMIT, Long.toString(logSizeLimit));
+        ENTRY_LOG_SIZE_LIMIT_KEY.set(this, logSizeLimit);
         return this;
     }
 
@@ -2367,7 +2415,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * @return the class name
      */
     public String getLedgerStorageClass() {
-        String ledgerStorageClass = getString(LEDGER_STORAGE_CLASS, SortedLedgerStorage.class.getName());
+        String ledgerStorageClass = LEDGER_STORAGE_CLASS_KEY.getString(this);
         if (ledgerStorageClass.equals(SortedLedgerStorage.class.getName())
                 && !getSortedLedgerStorageEnabled()) {
             // This is to retain compatibility with BK-4.3 configuration
@@ -2390,7 +2438,7 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * @return ServerConfiguration
      */
     public ServerConfiguration setLedgerStorageClass(String ledgerStorageClass) {
-        setProperty(LEDGER_STORAGE_CLASS, ledgerStorageClass);
+        LEDGER_STORAGE_CLASS_KEY.set(this, ledgerStorageClass);
         return this;
     }
 
@@ -2523,6 +2571,14 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
      * @throws ConfigurationException
      */
     public void validate() throws ConfigurationException {
+        // generate config def
+        ConfigDef configDef = ConfigDef.of(ServerConfiguration.class);
+        try {
+            configDef.validate(this);
+        } catch (ConfigException e) {
+            throw new ConfigurationException(e.getMessage(), e.getCause());
+        }
+
         if (getSkipListArenaChunkSize() < getSkipListArenaMaxAllocSize()) {
             throw new ConfigurationException("Arena max allocation size should be smaller than the chunk size.");
         }
@@ -2532,10 +2588,6 @@ public class ServerConfiguration extends AbstractConfiguration<ServerConfigurati
         if (getJournalAlignmentSize() > getJournalPreAllocSizeMB() * 1024 * 1024) {
             throw new ConfigurationException("Invalid preallocation size : " + getJournalPreAllocSizeMB() + " MB");
         }
-        if (getEntryLogSizeLimit() > BookKeeperConstants.MAX_LOG_SIZE_LIMIT) {
-            throw new ConfigurationException("Entry log file size should not be larger than "
-                    + BookKeeperConstants.MAX_LOG_SIZE_LIMIT);
-        }
         if (0 == getBookiePort() && !getAllowEphemeralPorts()) {
             throw new ConfigurationException("Invalid port specified, using ephemeral ports accidentally?");
         }
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
index 322cdd0..44f20e6 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/bookie/SortedLedgerStorageCheckpointTest.java
@@ -99,7 +99,7 @@ public class SortedLedgerStorageCheckpointTest extends LedgerStorageTestBase {
 
     public SortedLedgerStorageCheckpointTest() {
         super();
-        conf.setEntryLogSizeLimit(1);
+        conf.setEntryLogSizeLimit(1024);
         conf.setEntryLogFilePreAllocationEnabled(false);
         this.checkpoints = new LinkedBlockingQueue<>();
     }
diff --git a/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java b/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
index 424202d..fb139f5 100644
--- a/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
+++ b/bookkeeper-server/src/test/java/org/apache/bookkeeper/conf/TestServerConfiguration.java
@@ -22,8 +22,10 @@
 package org.apache.bookkeeper.conf;
 
 import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import org.apache.commons.configuration.ConfigurationException;
 import org.junit.Before;
@@ -111,4 +113,43 @@ public class TestServerConfiguration {
         conf.setFileInfoFormatVersionToWrite(1);
         conf.validate();
     }
+
+    @Test
+    public void testEntryLogSizeLimit() throws ConfigurationException {
+        ServerConfiguration conf = new ServerConfiguration();
+        try {
+            conf.setEntryLogSizeLimit(-1);
+            fail("should fail setEntryLogSizeLimit since `logSizeLimit` is too small");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+        try {
+            conf.setProperty("logSizeLimit", "-1");
+            conf.validate();
+            fail("Invalid configuration since `logSizeLimit` is too small");
+        } catch (ConfigurationException ce) {
+            // expected
+        }
+
+        try {
+            conf.setEntryLogSizeLimit(2 * 1024 * 1024 * 1024L - 1);
+            fail("Should fail setEntryLogSizeLimit size `logSizeLimit` is too large");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+        try {
+            conf.validate();
+            fail("Invalid configuration since `logSizeLimit` is too large");
+        } catch (ConfigurationException ce) {
+            // expected
+        }
+
+        conf.setEntryLogSizeLimit(512 * 1024 * 1024);
+        conf.validate();
+        assertEquals(512 * 1024 * 1024, conf.getEntryLogSizeLimit());
+
+        conf.setEntryLogSizeLimit(1073741824);
+        conf.validate();
+        assertEquals(1073741824, conf.getEntryLogSizeLimit());
+    }
 }
diff --git a/pom.xml b/pom.xml
index 125f2e2..c0c2c1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -996,6 +996,9 @@
             <exclude>**/__pycache__/**</exclude>
             <exclude>**/bookkeeper.egg-info/**</exclude>
             <exclude>**/pip-selfcheck.json</exclude>
+
+            <!-- test resources -->
+            <exclude>**/test_conf_2.conf</exclude>
           </excludes>
           <consoleOutput>true</consoleOutput>
         </configuration>