diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6e7c734bfdae..86a39f264004 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -106,3 +106,18 @@ jobs:
       - name: 'Test'
         run: |
           ./gradlew --no-parallel --no-daemon testSlow
+
+  linux-checkerframework:
+    name: 'CheckerFramework (JDK 11)'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 50
+      - name: 'Set up JDK 11'
+        uses: actions/setup-java@v1
+        with:
+          java-version: 11
+      - name: 'Run CheckerFramework'
+        run: |
+          ./gradlew --no-parallel --no-daemon -PenableCheckerframework classes
diff --git a/bom/build.gradle.kts b/bom/build.gradle.kts
index ed4e16d52d4e..620d96f2f23f 100644
--- a/bom/build.gradle.kts
+++ b/bom/build.gradle.kts
@@ -46,6 +46,7 @@ dependencies {
         // In other words, marking dependency as "runtime" would avoid accidental
         // dependency on it during compilation
         apiv("com.beust:jcommander")
+        apiv("org.checkerframework:checker-qual", "checkerframework")
         apiv("com.datastax.cassandra:cassandra-driver-core")
         apiv("com.esri.geometry:esri-geometry-api")
         apiv("com.fasterxml.jackson.core:jackson-annotations", "jackson")
diff --git a/build.gradle.kts b/build.gradle.kts
index 73b07e16062a..b3b4a26a3c14 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -33,6 +33,7 @@ plugins {
     // Verification
     checkstyle
     calcite.buildext
+    id("org.checkerframework") apply false
     id("com.github.autostyle")
     id("org.nosphere.apache.rat")
     id("com.github.spotbugs")
@@ -63,6 +64,7 @@ val enableSpotBugs = props.bool("spotbugs")
 val skipCheckstyle by props()
 val skipAutostyle by props()
 val skipJavadoc by props()
+val enableCheckerframework by props()
 val enableMavenLocal by props()
 val enableGradleMetadata by props()
 // Inherited from stage-vote-release-plugin: skipSign, useGpgCmd
@@ -495,6 +497,30 @@ allprojects {
             signaturesFiles = files("$rootDir/src/main/config/forbidden-apis/signatures.txt")
         }
 
+        if (enableCheckerframework) {
+            apply(plugin = "org.checkerframework")
+            dependencies {
+                "checkerFramework"("org.checkerframework:checker:${"checkerframework".v}")
+                // CheckerFramework annotations might be used in the code as follows:
+                // dependencies {
+                //     "compileOnly"("org.checkerframework:checker-qual")
+                //     "testCompileOnly"("org.checkerframework:checker-qual")
+                // }
+                if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
+                    // only needed for JDK 8
+                    "checkerFrameworkAnnotatedJDK"("org.checkerframework:jdk8")
+                }
+            }
+            configure<org.checkerframework.gradle.plugin.CheckerFrameworkExtension> {
+                applyToSubprojects = false
+                skipVersionCheck = true
+                // See https://checkerframework.org/manual/#introduction
+                checkers.add("org.checkerframework.checker.nullness.NullnessChecker")
+                checkers.add("org.checkerframework.checker.optional.OptionalChecker")
+                checkers.add("org.checkerframework.checker.regex.RegexChecker")
+            }
+        }
+
         tasks {
             configureEach<Jar> {
                 manifest {
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4533d69ebb51..eb1147bb48f3 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -19,6 +19,7 @@ pluginManagement {
         fun String.v() = extra["$this.version"].toString()
         fun PluginDependenciesSpec.idv(id: String, key: String = id) = id(id) version key.v()
 
+        idv("org.checkerframework")
         idv("com.github.autostyle")
         idv("com.github.johnrengelman.shadow")
         idv("com.github.spotbugs")