diff --git a/lib/config/definitions.js b/lib/config/definitions.js
index 4966f0a976214caff87e964f298ab2d0eaa13f6b..e98004294d16efccb098942793f4900f62c089df 100644
--- a/lib/config/definitions.js
+++ b/lib/config/definitions.js
@@ -404,6 +404,7 @@ const options = [
       'docker',
       'hashicorp',
       'hex',
+      'ivy',
       'loose',
       'maven',
       'node',
diff --git a/lib/versioning/ivy/index.js b/lib/versioning/ivy/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..ffcc1e883c3310d8104977fb00b2a88c415d639a
--- /dev/null
+++ b/lib/versioning/ivy/index.js
@@ -0,0 +1,73 @@
+const {
+  equals,
+  getMajor,
+  getMinor,
+  getPatch,
+  isGreaterThan,
+  isSingleVersion,
+  isStable,
+  matches: mavenMatches,
+  maxSatisfyingVersion,
+  minSatisfyingVersion,
+  getNewValue,
+  sortVersions,
+} = require('../maven/index');
+
+const { TYPE_QUALIFIER, tokenize, isSubversion } = require('../maven/compare');
+
+const {
+  REV_TYPE_LATEST,
+  REV_TYPE_SUBREV,
+  parseDynamicRevision,
+} = require('./parse');
+
+function isVersion(str) {
+  if (!str) {
+    return false;
+  }
+  return isSingleVersion(str) || !!parseDynamicRevision(str);
+}
+
+function matches(a, b) {
+  if (!a) return false;
+  if (!b) return false;
+  const dynamicRevision = parseDynamicRevision(b);
+  if (!dynamicRevision) return equals(a, b);
+  const { type, value } = dynamicRevision;
+
+  if (type === REV_TYPE_LATEST) {
+    if (!value) return true;
+    const tokens = tokenize(a);
+    if (tokens.length) {
+      const token = tokens[tokens.length - 1];
+      if (token.type === TYPE_QUALIFIER) {
+        return token.val.toLowerCase() === value;
+      }
+    }
+    return false;
+  }
+
+  if (type === REV_TYPE_SUBREV) {
+    return isSubversion(value, a);
+  }
+
+  return mavenMatches(a, value);
+}
+
+module.exports = {
+  equals,
+  getMajor,
+  getMinor,
+  getPatch,
+  isCompatible: isVersion,
+  isGreaterThan,
+  isSingleVersion,
+  isStable,
+  isValid: isVersion,
+  isVersion,
+  matches,
+  maxSatisfyingVersion,
+  minSatisfyingVersion,
+  getNewValue,
+  sortVersions,
+};
diff --git a/lib/versioning/ivy/parse.js b/lib/versioning/ivy/parse.js
new file mode 100644
index 0000000000000000000000000000000000000000..d08d80d18dc19e80fec67e759874bedf9c3cfec9
--- /dev/null
+++ b/lib/versioning/ivy/parse.js
@@ -0,0 +1,46 @@
+const { isSingleVersion, parseRange, rangeToStr } = require('../maven/compare');
+
+const REV_TYPE_LATEST = 'REV_TYPE_LATEST';
+const REV_TYPE_SUBREV = 'REV_TYPE_SUBREVISION';
+const REV_TYPE_RANGE = 'REV_TYPE_RANGE';
+
+function parseDynamicRevision(str) {
+  if (!str) return null;
+
+  const LATEST_REGEX = /^latest\.|^latest$/i;
+  if (LATEST_REGEX.test(str)) {
+    const value = str.replace(LATEST_REGEX, '').toLowerCase() || null;
+    return {
+      type: REV_TYPE_LATEST,
+      value: value !== 'integration' ? value : null,
+    };
+  }
+
+  const SUBREV_REGEX = /\.\+$/;
+  if (SUBREV_REGEX.test(str)) {
+    const value = str.replace(SUBREV_REGEX, '');
+    if (isSingleVersion(value)) {
+      return {
+        type: REV_TYPE_SUBREV,
+        value,
+      };
+    }
+  }
+
+  const range = parseRange(str);
+  if (range && range.length === 1) {
+    return {
+      type: REV_TYPE_RANGE,
+      value: rangeToStr(range),
+    };
+  }
+
+  return null;
+}
+
+module.exports = {
+  REV_TYPE_LATEST,
+  REV_TYPE_SUBREV,
+  REV_TYPE_RANGE,
+  parseDynamicRevision,
+};
diff --git a/lib/versioning/maven/compare.js b/lib/versioning/maven/compare.js
index 5123672f7da719a77079b3571800b6b6f2dedfd9..e5efe85802570d22a2a8aeb06cae52e4ec4483b0 100644
--- a/lib/versioning/maven/compare.js
+++ b/lib/versioning/maven/compare.js
@@ -240,8 +240,10 @@ function parseRange(rangeStr) {
     return {
       leftType: null,
       leftValue: null,
+      leftBracket: null,
       rightType: null,
       rightValue: null,
+      rightBracket: null,
     };
   }
 
@@ -257,18 +259,22 @@ function parseRange(rangeStr) {
         result.push({
           leftType: INCLUDING_POINT,
           leftValue: ver,
+          leftBracket: '[',
           rightType: INCLUDING_POINT,
           rightValue: ver,
+          rightBracket: ']',
         });
         interval = emptyInterval();
       } else if (subStr[0] === '[') {
         const ver = subStr.slice(1);
         interval.leftType = INCLUDING_POINT;
         interval.leftValue = ver;
-      } else if (subStr[0] === '(') {
+        interval.leftBracket = '[';
+      } else if (subStr[0] === '(' || subStr[0] === ']') {
         const ver = subStr.slice(1);
         interval.leftType = EXCLUDING_POINT;
         interval.leftValue = ver;
+        interval.leftBracket = subStr[0];
       } else {
         result = null;
       }
@@ -276,12 +282,14 @@ function parseRange(rangeStr) {
       const ver = subStr.slice(0, -1);
       interval.rightType = INCLUDING_POINT;
       interval.rightValue = ver;
+      interval.rightBracket = ']';
       result.push(interval);
       interval = emptyInterval();
-    } else if (/\)$/.test(subStr)) {
+    } else if (/\)$/.test(subStr) || /\[$/.test(subStr)) {
       const ver = subStr.slice(0, -1);
       interval.rightType = EXCLUDING_POINT;
       interval.rightValue = ver;
+      interval.rightBracket = /\)$/.test(subStr) ? ')' : '[';
       result.push(interval);
       interval = emptyInterval();
     } else {
@@ -324,26 +332,26 @@ function parseRange(rangeStr) {
 function rangeToStr(fullRange) {
   if (fullRange === null) return null;
 
-  const leftBracket = val => (val.leftType === INCLUDING_POINT ? '[' : '(');
-  const rightBracket = val => (val.rightType === INCLUDING_POINT ? ']' : ')');
   const valToStr = val => (val === null ? '' : val);
 
   if (fullRange.length === 1) {
-    const val = fullRange[0];
-    if (val.leftValue === val.rightValue) {
-      return `${leftBracket(val)}${valToStr(val.leftValue)}${rightBracket(
-        val
-      )}`;
+    const { leftBracket, rightBracket, leftValue, rightValue } = fullRange[0];
+    if (
+      leftValue === rightValue &&
+      leftBracket === '[' &&
+      rightBracket === ']'
+    ) {
+      return `[${valToStr(leftValue)}]`;
     }
   }
 
   const intervals = fullRange.map(val =>
     [
-      leftBracket(val),
+      val.leftBracket,
       valToStr(val.leftValue),
       ',',
       valToStr(val.rightValue),
-      rightBracket(val),
+      val.rightBracket,
     ].join('')
   );
   return intervals.join(',');
@@ -389,12 +397,31 @@ function autoExtendMavenRange(currentRepresentation, newValue) {
   return rangeToStr(range);
 }
 
+function isSubversion(majorVersion, minorVersion) {
+  const majorTokens = tokenize(majorVersion);
+  const minorTokens = tokenize(minorVersion);
+
+  let result = true;
+  const len = majorTokens.length;
+  for (let idx = 0; idx < len; idx += 1) {
+    const major = majorTokens[idx];
+    const minor = minorTokens[idx] || nullFor(majorTokens[idx]);
+    const cmpResult = tokenCmp(major, minor);
+    if (cmpResult !== 0) {
+      result = false;
+      break;
+    }
+  }
+  return result;
+}
+
 module.exports = {
   PREFIX_DOT,
   PREFIX_HYPHEN,
   TYPE_NUMBER,
   TYPE_QUALIFIER,
   tokenize,
+  isSubversion,
   compare,
   isSingleVersion,
   isVersion,
diff --git a/renovate-schema.json b/renovate-schema.json
index c2bb05f900260468940fa2aaad7409c847e3491d..8179476d634016a53f7561a5488766e138fd5ae5 100644
--- a/renovate-schema.json
+++ b/renovate-schema.json
@@ -260,6 +260,7 @@
         "docker",
         "hashicorp",
         "hex",
+        "ivy",
         "loose",
         "maven",
         "node",
diff --git a/test/versioning/ivy.spec.js b/test/versioning/ivy.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b9e86e05ce42d3389749f0019f0741b6dd710e18
--- /dev/null
+++ b/test/versioning/ivy.spec.js
@@ -0,0 +1,128 @@
+const {
+  REV_TYPE_LATEST,
+  REV_TYPE_SUBREV,
+  REV_TYPE_RANGE,
+  parseDynamicRevision,
+} = require('../../lib/versioning/ivy/parse');
+
+const { isVersion, matches } = require('../../lib/versioning/ivy/index');
+
+describe('versioning/ivy/match', () => {
+  it('parses dynamic revisions', () => {
+    expect(parseDynamicRevision(null)).toBeNull();
+    expect(parseDynamicRevision('')).toBeNull();
+
+    expect(parseDynamicRevision('latest')).toEqual({
+      type: REV_TYPE_LATEST,
+      value: null,
+    });
+    expect(parseDynamicRevision('latest.release')).toEqual({
+      type: REV_TYPE_LATEST,
+      value: 'release',
+    });
+    expect(parseDynamicRevision('latest.milestone')).toEqual({
+      type: REV_TYPE_LATEST,
+      value: 'milestone',
+    });
+    expect(parseDynamicRevision('latest.integration')).toEqual({
+      type: REV_TYPE_LATEST,
+      value: null,
+    });
+
+    expect(parseDynamicRevision('.+')).toBeNull();
+    expect(parseDynamicRevision('1.0.+')).toEqual({
+      type: REV_TYPE_SUBREV,
+      value: '1.0',
+    });
+    expect(parseDynamicRevision('1.2.3.+')).toEqual({
+      type: REV_TYPE_SUBREV,
+      value: '1.2.3',
+    });
+
+    [
+      '[1.0,2.0]',
+      '[1.0,2.0[',
+      ']1.0,2.0]',
+      ']1.0,2.0[',
+      '[1.0,)',
+      ']1.0,)',
+      '(,2.0]',
+      '(,2.0[',
+    ].forEach(value => {
+      expect(parseDynamicRevision(value)).toEqual({
+        type: REV_TYPE_RANGE,
+        value,
+      });
+    });
+
+    expect(parseDynamicRevision('[0,1),(1,)')).toBeNull();
+  });
+});
+
+describe('versioning/ivy/index', () => {
+  it('validates version string', () => {
+    expect(isVersion('')).toBe(false);
+    expect(isVersion('1.0.0')).toBe(true);
+    expect(isVersion('0')).toBe(true);
+    expect(isVersion('0.1-2-sp')).toBe(true);
+    expect(isVersion('1-final')).toBe(true);
+    expect(isVersion('v1.0.0')).toBe(true);
+    expect(isVersion('x1.0.0')).toBe(true);
+    expect(isVersion('2.1.1.RELEASE')).toBe(true);
+    expect(isVersion('Greenwich.SR1')).toBe(true);
+    expect(isVersion('.1')).toBe(false);
+    expect(isVersion('1.')).toBe(false);
+    expect(isVersion('-1')).toBe(false);
+    expect(isVersion('1-')).toBe(false);
+
+    expect(isVersion('latest')).toBe(true);
+    expect(isVersion('latest.release')).toBe(true);
+    expect(isVersion('latest.milestone')).toBe(true);
+    expect(isVersion('latest.integration')).toBe(true);
+    expect(isVersion('1.0.+')).toBe(true);
+    expect(isVersion('1.0+')).toBe(false);
+    expect(isVersion(']0,1[')).toBe(true);
+    expect(isVersion('[0,1]')).toBe(true);
+    expect(isVersion('[0,1),(1,2]')).toBe(false);
+  });
+  it('matches against dynamic revisions', () => {
+    expect(matches('', 'latest')).toBe(false);
+    expect(matches('0', '')).toBe(false);
+    expect(matches('0', 'latest')).toBe(true);
+    expect(matches('0', 'latest.integration')).toBe(true);
+
+    expect(matches('0', 'latest.release')).toBe(false);
+    expect(matches('release', 'latest.release')).toBe(true);
+    expect(matches('0.release', 'latest.release')).toBe(true);
+    expect(matches('0-release', 'latest.release')).toBe(true);
+    expect(matches('0release', 'latest.release')).toBe(true);
+    expect(matches('0.RELEASE', 'latest.release')).toBe(true);
+
+    expect(matches('0', 'latest.milestone')).toBe(false);
+    expect(matches('milestone', 'latest.milestone')).toBe(true);
+    expect(matches('0.milestone', 'latest.milestone')).toBe(true);
+    expect(matches('0-milestone', 'latest.milestone')).toBe(true);
+    expect(matches('0milestone', 'latest.milestone')).toBe(true);
+    expect(matches('0.MILESTONE', 'latest.milestone')).toBe(true);
+
+    expect(matches('0', '1.0.+')).toBe(false);
+    expect(matches('1.1.0', '1.2.+')).toBe(false);
+    expect(matches('1.2.0', '1.2.+')).toBe(true);
+    expect(matches('1.2.milestone', '1.2.+')).toBe(true);
+    expect(matches('1.3', '1.2.+')).toBe(false);
+
+    expect(matches('1', '1')).toBe(true);
+    expect(matches('1', '0')).toBe(false);
+    expect(matches('1', '[0,1]')).toBe(true);
+    expect(matches('0', '(0,1)')).toBe(false);
+    expect(matches('0', '(0,1[')).toBe(false);
+    expect(matches('0', ']0,1)')).toBe(false);
+    expect(matches('1', '(0,1)')).toBe(false);
+    expect(matches('1', '(0,2)')).toBe(true);
+    expect(matches('1', '[0,2]')).toBe(true);
+    expect(matches('1', '(,1]')).toBe(true);
+    expect(matches('1', '(,1)')).toBe(false);
+    expect(matches('1', '[1,)')).toBe(true);
+    expect(matches('1', '(1,)')).toBe(false);
+  });
+});
diff --git a/test/versioning/maven.spec.js b/test/versioning/maven.spec.js
index 8a822ef85be39448cfbaf29aed00b445764c8595..4e7ee9438b6818f2e870a62c37470ac8b6062df9 100644
--- a/test/versioning/maven.spec.js
+++ b/test/versioning/maven.spec.js
@@ -122,68 +122,86 @@ describe('versioning/maven/compare', () => {
         {
           leftType: 'INCLUDING_POINT',
           leftValue: '1.0',
+          leftBracket: '[',
           rightType: 'INCLUDING_POINT',
           rightValue: '1.0',
+          rightBracket: ']',
         },
       ],
       '(,1.0]': [
         {
           leftType: 'EXCLUDING_POINT',
           leftValue: null,
+          leftBracket: '(',
           rightType: 'INCLUDING_POINT',
           rightValue: '1.0',
+          rightBracket: ']',
         },
       ],
       '[1.2,1.3]': [
         {
           leftType: 'INCLUDING_POINT',
           leftValue: '1.2',
+          leftBracket: '[',
           rightType: 'INCLUDING_POINT',
           rightValue: '1.3',
+          rightBracket: ']',
         },
       ],
       '[1.0,2.0)': [
         {
           leftType: 'INCLUDING_POINT',
           leftValue: '1.0',
+          leftBracket: '[',
           rightType: 'EXCLUDING_POINT',
           rightValue: '2.0',
+          rightBracket: ')',
         },
       ],
       '[1.5,)': [
         {
           leftType: 'INCLUDING_POINT',
           leftValue: '1.5',
+          leftBracket: '[',
           rightType: 'EXCLUDING_POINT',
           rightValue: null,
+          rightBracket: ')',
         },
       ],
       '(,1.0],[1.2,)': [
         {
           leftType: 'EXCLUDING_POINT',
           leftValue: null,
+          leftBracket: '(',
           rightType: 'INCLUDING_POINT',
           rightValue: '1.0',
+          rightBracket: ']',
         },
         {
           leftType: 'INCLUDING_POINT',
           leftValue: '1.2',
+          leftBracket: '[',
           rightType: 'EXCLUDING_POINT',
           rightValue: null,
+          rightBracket: ')',
         },
       ],
       '(,1.1),(1.1,)': [
         {
           leftType: 'EXCLUDING_POINT',
           leftValue: null,
+          leftBracket: '(',
           rightType: 'EXCLUDING_POINT',
           rightValue: '1.1',
+          rightBracket: ')',
         },
         {
           leftType: 'EXCLUDING_POINT',
           leftValue: '1.1',
+          leftBracket: '(',
           rightType: 'EXCLUDING_POINT',
           rightValue: null,
+          rightBracket: ')',
         },
       ],
     };
@@ -205,9 +223,13 @@ describe('versioning/maven/compare', () => {
       ['[1.0.0,1.2.3]', '1.2.4', '[1.0.0,1.2.4]'],
       ['[1.0.0,1.2.23]', '1.1.0', '[1.0.0,1.2.23]'],
       ['(,1.0]', '2.0', '(,2.0]'],
+      ['],1.0]', '2.0', '],2.0]'],
       ['(,1.0)', '2.0', '(,2.0)'],
+      ['],1.0[', '2.0', '],2.0['],
       ['[1.0,1.2],[1.3,1.5)', '1.2.4', '[1.0,1.2.4],[1.3,1.5)'],
+      ['[1.0,1.2],[1.3,1.5[', '1.2.4', '[1.0,1.2.4],[1.3,1.5['],
       ['[1.2.3,)', '1.2.4', '[1.2.4,)'],
+      ['[1.2.3,[', '1.2.4', '[1.2.4,['],
       ['[1.2.3,]', '1.2.4', '[1.2.3,]'], // invalid range
     ];
     sample.forEach(([oldRepr, newValue, newRepr]) => {