Merge "Remove redundant type arguments"
diff --git a/Documentation/dev-bazel.txt b/Documentation/dev-bazel.txt
index 9792d8e..b73c404 100644
--- a/Documentation/dev-bazel.txt
+++ b/Documentation/dev-bazel.txt
@@ -369,6 +369,7 @@
 * git-protocol-v2
 * git-upload-archive
 * notedb
+* no_rbe
 * pgm
 * rest
 * server
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 694e5d8..a24d80d 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -264,6 +264,11 @@
 often combined with 'branch:' and 'project:' operators to select
 all related changes in a series.
 
+[[prefixtopic]]
+prefixtopic:'TOPIC'::
++
+Changes whose designated topic start with 'TOPIC'.
+
 [[inhashtag]]
 inhashtag:'HASHTAG'::
 +
@@ -280,6 +285,12 @@
 Changes whose link:intro-user.html#hashtags[hashtag] matches 'HASHTAG'.
 The match is case-insensitive.
 
+[[prefixhashtag]]
+prefixhashtag:'HASHTAG'::
++
+Changes whose link:intro-user.html#hashtags[hashtag] start with 'HASHTAG'.
+The match is case-insensitive.
+
 [[cherrypickof]]
 cherrypickof:'CHANGE[,PATCHSET]'::
 +
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
index 3b15b57..580f10f 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/ChangeOperationsImpl.java
@@ -48,8 +48,8 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.Objects;
 import java.util.Optional;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -138,7 +138,7 @@
     try (Repository repository = repositoryManager.openRepository(project);
         ObjectInserter objectInserter = repository.newObjectInserter();
         RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Timestamp now = TimeUtil.nowTs();
+      Instant now = TimeUtil.now();
       IdentifiedUser changeOwner = getChangeOwner(changeCreation);
       PersonIdent authorAndCommitter =
           changeOwner.newCommitterIdent(now, serverIdent.getTimeZone());
@@ -431,7 +431,7 @@
       try (Repository repository = repositoryManager.openRepository(project);
           ObjectInserter objectInserter = repository.newObjectInserter();
           RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-        Timestamp now = TimeUtil.nowTs();
+        Instant now = TimeUtil.now();
         ObjectId newPatchsetCommit =
             createPatchsetCommit(
                 repository, revWalk, objectInserter, changeNotes, patchsetCreation, now);
@@ -457,7 +457,7 @@
         ObjectInserter objectInserter,
         ChangeNotes changeNotes,
         TestPatchsetCreation patchsetCreation,
-        Timestamp now)
+        Instant now)
         throws IOException, BadRequestException {
       ObjectId oldPatchsetCommitId = changeNotes.getCurrentPatchSet().commitId();
       RevCommit oldPatchsetCommit = repository.parseCommit(oldPatchsetCommitId);
@@ -494,10 +494,13 @@
       return Optional.ofNullable(oldPatchsetCommit.getAuthorIdent()).orElse(serverIdent);
     }
 
-    private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Timestamp now) {
+    // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+    // Instants
+    @SuppressWarnings("JdkObsolete")
+    private PersonIdent getCommitter(RevCommit oldPatchsetCommit, Instant now) {
       PersonIdent oldPatchsetCommitter =
           Optional.ofNullable(oldPatchsetCommit.getCommitterIdent()).orElse(serverIdent);
-      if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen())) {
+      if (asSeconds(now) == asSeconds(oldPatchsetCommitter.getWhen().toInstant())) {
         /* We need to ensure that the resulting commit SHA-1 is different from the old patchset.
          * In real situations, this automatically happens as two patchsets won't have exactly the
          * same commit timestamp even when the tree and commit message are the same. In tests,
@@ -505,13 +508,13 @@
          * We could of course require that tests must use TestTimeUtil#setClockStep but
          * that would be an unnecessary nuisance for test writers. Hence, go with a simple solution
          * here and simply add a second. */
-        now = Timestamp.from(now.toInstant().plusSeconds(1));
+        now = now.plusSeconds(1);
       }
-      return new PersonIdent(oldPatchsetCommitter, now);
+      return new PersonIdent(oldPatchsetCommitter, Timestamp.from(now));
     }
 
-    private long asSeconds(Date date) {
-      return date.getTime() / 1000;
+    private long asSeconds(Instant date) {
+      return date.getEpochSecond();
     }
 
     private ImmutableList<ObjectId> getParents(
diff --git a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
index eda6c7e..9b393ef 100644
--- a/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
+++ b/java/com/google/gerrit/acceptance/testsuite/change/PerPatchsetOperationsImpl.java
@@ -38,7 +38,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -106,7 +106,7 @@
     try (Repository repository = repositoryManager.openRepository(project);
         ObjectInserter objectInserter = repository.newObjectInserter();
         RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Timestamp now = TimeUtil.nowTs();
+      Instant now = TimeUtil.now();
 
       IdentifiedUser author = getAuthor(commentCreation);
       CommentAdditionOp commentAdditionOp = new CommentAdditionOp(commentCreation);
@@ -165,8 +165,7 @@
       short side = commentCreation.side().orElse(CommentSide.PATCHSET_COMMIT).getNumericSide();
       Boolean unresolved = commentCreation.unresolved().orElse(null);
       String parentUuid = commentCreation.parentUuid().orElse(null);
-      Timestamp createdOn =
-          commentCreation.createdOn().map(Timestamp::from).orElse(context.getWhen());
+      Instant createdOn = commentCreation.createdOn().orElse(context.getWhen());
       HumanComment newComment =
           commentsUtil.newHumanComment(
               context.getNotes(),
@@ -202,7 +201,7 @@
     try (Repository repository = repositoryManager.openRepository(project);
         ObjectInserter objectInserter = repository.newObjectInserter();
         RevWalk revWalk = new RevWalk(objectInserter.newReader())) {
-      Timestamp now = TimeUtil.nowTs();
+      Instant now = TimeUtil.now();
 
       IdentifiedUser author = getAuthor(robotCommentCreation);
       RobotCommentAdditionOp robotCommentAdditionOp =
diff --git a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
index c885353..0b21e2c 100644
--- a/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
+++ b/java/com/google/gerrit/acceptance/testsuite/group/TestGroup.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 @AutoValue
@@ -40,7 +40,7 @@
 
   public abstract boolean visibleToAll();
 
-  public abstract Timestamp createdOn();
+  public abstract Instant createdOn();
 
   public abstract ImmutableSet<Account.Id> members();
 
@@ -67,7 +67,7 @@
 
     public abstract Builder visibleToAll(boolean visibleToAll);
 
-    public abstract Builder createdOn(Timestamp createdOn);
+    public abstract Builder createdOn(Instant createdOn);
 
     public abstract Builder members(ImmutableSet<Account.Id> members);
 
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index 18fcef3..303e79f 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -22,7 +22,7 @@
 import com.google.common.primitives.Ints;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 /**
@@ -123,7 +123,7 @@
   public abstract Id id();
 
   /** Date and time the user registered with the review server. */
-  public abstract Timestamp registeredOn();
+  public abstract Instant registeredOn();
 
   /** Full name of the user ("Given-name Surname" style). */
   @Nullable
@@ -157,7 +157,7 @@
    * @param newId unique id, see {@link com.google.gerrit.server.notedb.Sequences#nextAccountId()}.
    * @param registeredOn when the account was registered.
    */
-  public static Account.Builder builder(Account.Id newId, Timestamp registeredOn) {
+  public static Account.Builder builder(Account.Id newId, Instant registeredOn) {
     return new AutoValue_Account.Builder()
         .setInactive(false)
         .setId(newId)
@@ -230,9 +230,9 @@
 
     abstract Builder setId(Id id);
 
-    public abstract Timestamp registeredOn();
+    public abstract Instant registeredOn();
 
-    abstract Builder setRegisteredOn(Timestamp registeredOn);
+    abstract Builder setRegisteredOn(Instant registeredOn);
 
     @Nullable
     public abstract String fullName();
diff --git a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
index 17ddf51..0ef51e5 100644
--- a/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupByIdAudit.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 /** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
@@ -33,13 +33,13 @@
 
     public abstract Builder addedBy(Account.Id addedBy);
 
-    public abstract Builder addedOn(Timestamp addedOn);
+    public abstract Builder addedOn(Instant addedOn);
 
     abstract Builder removedBy(Account.Id removedBy);
 
-    abstract Builder removedOn(Timestamp removedOn);
+    abstract Builder removedOn(Instant removedOn);
 
-    public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+    public Builder removed(Account.Id removedBy, Instant removedOn) {
       return removedBy(removedBy).removedOn(removedOn);
     }
 
@@ -52,11 +52,11 @@
 
   public abstract Account.Id addedBy();
 
-  public abstract Timestamp addedOn();
+  public abstract Instant addedOn();
 
   public abstract Optional<Account.Id> removedBy();
 
-  public abstract Optional<Timestamp> removedOn();
+  public abstract Optional<Instant> removedOn();
 
   public abstract Builder toBuilder();
 
diff --git a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
index 4d191b8..913956e 100644
--- a/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
+++ b/java/com/google/gerrit/entities/AccountGroupMemberAudit.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.auto.value.AutoValue;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 /** Membership of an {@link Account} in an {@link AccountGroup}. */
@@ -35,15 +35,15 @@
 
     abstract Account.Id addedBy();
 
-    public abstract Builder addedOn(Timestamp addedOn);
+    public abstract Builder addedOn(Instant addedOn);
 
-    abstract Timestamp addedOn();
+    abstract Instant addedOn();
 
     abstract Builder removedBy(Account.Id removedBy);
 
-    abstract Builder removedOn(Timestamp removedOn);
+    abstract Builder removedOn(Instant removedOn);
 
-    public Builder removed(Account.Id removedBy, Timestamp removedOn) {
+    public Builder removed(Account.Id removedBy, Instant removedOn) {
       return removedBy(removedBy).removedOn(removedOn);
     }
 
@@ -60,11 +60,11 @@
 
   public abstract Account.Id addedBy();
 
-  public abstract Timestamp addedOn();
+  public abstract Instant addedOn();
 
   public abstract Optional<Account.Id> removedBy();
 
-  public abstract Optional<Timestamp> removedOn();
+  public abstract Optional<Instant> removedOn();
 
   public abstract Builder toBuilder();
 
diff --git a/java/com/google/gerrit/entities/Change.java b/java/com/google/gerrit/entities/Change.java
index 71684d3..7ca235d 100644
--- a/java/com/google/gerrit/entities/Change.java
+++ b/java/com/google/gerrit/entities/Change.java
@@ -24,7 +24,7 @@
 import com.google.gson.Gson;
 import com.google.gson.TypeAdapter;
 import com.google.gson.annotations.SerializedName;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Optional;
 
@@ -444,14 +444,14 @@
   private Key changeKey;
 
   /** When this change was first introduced into the database. */
-  private Timestamp createdOn;
+  private Instant createdOn;
 
   /**
    * When was a meaningful modification last made to this record's data
    *
    * <p>Note, this update timestamp includes its children.
    */
-  private Timestamp lastUpdatedOn;
+  private Instant lastUpdatedOn;
 
   private Account.Id owner;
 
@@ -505,11 +505,7 @@
   Change() {}
 
   public Change(
-      Change.Key newKey,
-      Change.Id newId,
-      Account.Id ownedBy,
-      BranchNameKey forBranch,
-      Timestamp ts) {
+      Change.Key newKey, Change.Id newId, Account.Id ownedBy, BranchNameKey forBranch, Instant ts) {
     changeKey = newKey;
     changeId = newId;
     createdOn = ts;
@@ -567,19 +563,19 @@
     assignee = a;
   }
 
-  public Timestamp getCreatedOn() {
+  public Instant getCreatedOn() {
     return createdOn;
   }
 
-  public void setCreatedOn(Timestamp ts) {
+  public void setCreatedOn(Instant ts) {
     createdOn = ts;
   }
 
-  public Timestamp getLastUpdatedOn() {
+  public Instant getLastUpdatedOn() {
     return lastUpdatedOn;
   }
 
-  public void setLastUpdatedOn(Timestamp now) {
+  public void setLastUpdatedOn(Instant now) {
     lastUpdatedOn = now;
   }
 
diff --git a/java/com/google/gerrit/entities/ChangeMessage.java b/java/com/google/gerrit/entities/ChangeMessage.java
index cab3290..609b54c 100644
--- a/java/com/google/gerrit/entities/ChangeMessage.java
+++ b/java/com/google/gerrit/entities/ChangeMessage.java
@@ -16,7 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.common.Nullable;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -46,7 +46,7 @@
   @Nullable private Account.Id author;
 
   /** When this comment was drafted. */
-  private Timestamp writtenOn;
+  private Instant writtenOn;
 
   /**
    * The text left by the user or Gerrit system in template form, that is free of Gerrit User
@@ -66,14 +66,14 @@
   private ChangeMessage() {}
 
   public static ChangeMessage create(
-      final ChangeMessage.Key k, @Nullable Account.Id a, Timestamp wo, @Nullable PatchSet.Id psid) {
+      final ChangeMessage.Key k, @Nullable Account.Id a, Instant wo, @Nullable PatchSet.Id psid) {
     return create(k, a, wo, psid, /*messageTemplate=*/ null, /*realAuthor=*/ null, /*tag=*/ null);
   }
 
   public static ChangeMessage create(
       final ChangeMessage.Key k,
       @Nullable Account.Id a,
-      Timestamp wo,
+      Instant wo,
       @Nullable PatchSet.Id psid,
       @Nullable String messageTemplate,
       @Nullable Account.Id realAuthor,
@@ -103,7 +103,7 @@
     return realAuthor != null ? realAuthor : getAuthor();
   }
 
-  public Timestamp getWrittenOn() {
+  public Instant getWrittenOn() {
     return writtenOn;
   }
 
diff --git a/java/com/google/gerrit/entities/Comment.java b/java/com/google/gerrit/entities/Comment.java
index 92bcaf6..65a1559 100644
--- a/java/com/google/gerrit/entities/Comment.java
+++ b/java/com/google/gerrit/entities/Comment.java
@@ -18,6 +18,7 @@
 import com.google.common.base.MoreObjects.ToStringHelper;
 import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Comparator;
 import java.util.Objects;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -215,7 +216,10 @@
 
   public Identity author;
   protected Identity realAuthor;
+
+  // TODO(issue-15525): Migrate this field from Timestamp to Instant
   public Timestamp writtenOn;
+
   public short side;
   public String message;
   public String parentUuid;
@@ -233,13 +237,7 @@
   public String serverId;
 
   public Comment(Comment c) {
-    this(
-        new Key(c.key),
-        c.author.getId(),
-        new Timestamp(c.writtenOn.getTime()),
-        c.side,
-        c.message,
-        c.serverId);
+    this(new Key(c.key), c.author.getId(), c.writtenOn.toInstant(), c.side, c.message, c.serverId);
     this.lineNbr = c.lineNbr;
     this.realAuthor = c.realAuthor;
     this.parentUuid = c.parentUuid;
@@ -249,21 +247,20 @@
   }
 
   public Comment(
-      Key key,
-      Account.Id author,
-      Timestamp writtenOn,
-      short side,
-      String message,
-      String serverId) {
+      Key key, Account.Id author, Instant writtenOn, short side, String message, String serverId) {
     this.key = key;
     this.author = new Comment.Identity(author);
     this.realAuthor = this.author;
-    this.writtenOn = writtenOn;
+    this.writtenOn = Timestamp.from(writtenOn);
     this.side = side;
     this.message = message;
     this.serverId = serverId;
   }
 
+  public void setWrittenOn(Instant writtenOn) {
+    this.writtenOn = Timestamp.from(writtenOn);
+  }
+
   public void setLineNbrAndRange(
       Integer lineNbr, com.google.gerrit.extensions.client.Comment.Range range) {
     this.lineNbr = lineNbr != null ? lineNbr : range != null ? range.endLine : 0;
diff --git a/java/com/google/gerrit/entities/EmailHeader.java b/java/com/google/gerrit/entities/EmailHeader.java
index d2be65e..e43b6a3 100644
--- a/java/com/google/gerrit/entities/EmailHeader.java
+++ b/java/com/google/gerrit/entities/EmailHeader.java
@@ -19,7 +19,9 @@
 import com.google.common.base.MoreObjects;
 import java.io.IOException;
 import java.io.Writer;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -127,13 +129,13 @@
   }
 
   public static class Date extends EmailHeader {
-    private final java.util.Date value;
+    private final Instant value;
 
-    public Date(java.util.Date v) {
+    public Date(Instant v) {
       value = v;
     }
 
-    public java.util.Date getDate() {
+    public Instant getDate() {
       return value;
     }
 
@@ -144,10 +146,12 @@
 
     @Override
     public void write(Writer w) throws IOException {
-      final SimpleDateFormat fmt;
-      // Mon, 1 Jun 2009 10:49:44 -0700
-      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.US);
-      w.write(fmt.format(value));
+      // Mon, 1 Jun 2009 10:49:44 +0000
+      w.write(
+          DateTimeFormatter.ofPattern("EEE, d MMM yyyy HH:mm:ss Z")
+              .withLocale(Locale.US)
+              .withZone(ZoneId.of("UTC"))
+              .format(value));
     }
 
     @Override
@@ -157,7 +161,7 @@
 
     @Override
     public boolean equals(Object o) {
-      return (o instanceof Date) && value.getTime() == ((Date) o).value.getTime();
+      return (o instanceof Date) && Objects.equals(value, ((Date) o).value);
     }
 
     @Override
diff --git a/java/com/google/gerrit/entities/GroupDescription.java b/java/com/google/gerrit/entities/GroupDescription.java
index 7054bed..666e8f6 100644
--- a/java/com/google/gerrit/entities/GroupDescription.java
+++ b/java/com/google/gerrit/entities/GroupDescription.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.entities;
 
 import com.google.gerrit.common.Nullable;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Set;
 
 /** Group methods exposed by the GroupBackend. */
@@ -55,7 +55,7 @@
 
     boolean isVisibleToAll();
 
-    Timestamp getCreatedOn();
+    Instant getCreatedOn();
 
     Set<Account.Id> getMembers();
 
diff --git a/java/com/google/gerrit/entities/HumanComment.java b/java/com/google/gerrit/entities/HumanComment.java
index 50bee8d..d287fa0 100644
--- a/java/com/google/gerrit/entities/HumanComment.java
+++ b/java/com/google/gerrit/entities/HumanComment.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.entities;
 
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -33,7 +33,7 @@
   public HumanComment(
       Key key,
       Account.Id author,
-      Timestamp writtenOn,
+      Instant writtenOn,
       short side,
       String message,
       String serverId,
diff --git a/java/com/google/gerrit/entities/InternalGroup.java b/java/com/google/gerrit/entities/InternalGroup.java
index ebfa36a..43c3af3 100644
--- a/java/com/google/gerrit/entities/InternalGroup.java
+++ b/java/com/google/gerrit/entities/InternalGroup.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.Nullable;
 import java.io.Serializable;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.eclipse.jgit.lib.ObjectId;
 
 @AutoValue
@@ -42,7 +42,7 @@
 
   public abstract AccountGroup.UUID getGroupUUID();
 
-  public abstract Timestamp getCreatedOn();
+  public abstract Instant getCreatedOn();
 
   public abstract ImmutableSet<Account.Id> getMembers();
 
@@ -71,7 +71,7 @@
 
     public abstract Builder setGroupUUID(AccountGroup.UUID groupUuid);
 
-    public abstract Builder setCreatedOn(Timestamp createdOn);
+    public abstract Builder setCreatedOn(Instant createdOn);
 
     public abstract Builder setMembers(ImmutableSet<Account.Id> members);
 
diff --git a/java/com/google/gerrit/entities/PatchSet.java b/java/com/google/gerrit/entities/PatchSet.java
index acbf697d..6c52368 100644
--- a/java/com/google/gerrit/entities/PatchSet.java
+++ b/java/com/google/gerrit/entities/PatchSet.java
@@ -23,7 +23,7 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.primitives.Ints;
 import com.google.errorprone.annotations.InlineMe;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
@@ -163,7 +163,7 @@
 
     public abstract Builder uploader(Account.Id uploader);
 
-    public abstract Builder createdOn(Timestamp createdOn);
+    public abstract Builder createdOn(Instant createdOn);
 
     public abstract Builder groups(Iterable<String> groups);
 
@@ -210,7 +210,7 @@
    * Gerrit, and the old data erroneously did not include a {@code createdOn}, then this method will
    * return a timestamp of 0.
    */
-  public abstract Timestamp createdOn();
+  public abstract Instant createdOn();
 
   /**
    * Opaque group identifier, usually assigned during creation.
diff --git a/java/com/google/gerrit/entities/PatchSetApproval.java b/java/com/google/gerrit/entities/PatchSetApproval.java
index 607f5c8..608cf0d 100644
--- a/java/com/google/gerrit/entities/PatchSetApproval.java
+++ b/java/com/google/gerrit/entities/PatchSetApproval.java
@@ -16,8 +16,7 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.primitives.Shorts;
-import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Optional;
 
 /** An approval (or negative approval) on a patch set. */
@@ -97,11 +96,7 @@
       return value(Shorts.checkedCast(value));
     }
 
-    public abstract Builder granted(Timestamp granted);
-
-    public Builder granted(Date granted) {
-      return granted(new Timestamp(granted.getTime()));
-    }
+    public abstract Builder granted(Instant granted);
 
     public abstract Builder tag(String tag);
 
@@ -147,7 +142,7 @@
    */
   public abstract short value();
 
-  public abstract Timestamp granted();
+  public abstract Instant granted();
 
   public abstract Optional<String> tag();
 
diff --git a/java/com/google/gerrit/entities/RobotComment.java b/java/com/google/gerrit/entities/RobotComment.java
index e2e4114..1d46d3b 100644
--- a/java/com/google/gerrit/entities/RobotComment.java
+++ b/java/com/google/gerrit/entities/RobotComment.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.entities;
 
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -29,7 +29,7 @@
   public RobotComment(
       Key key,
       Account.Id author,
-      Timestamp writtenOn,
+      Instant writtenOn,
       short side,
       String message,
       String serverId,
diff --git a/java/com/google/gerrit/entities/UserIdentity.java b/java/com/google/gerrit/entities/UserIdentity.java
index ca092c1..8334157 100644
--- a/java/com/google/gerrit/entities/UserIdentity.java
+++ b/java/com/google/gerrit/entities/UserIdentity.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.entities;
 
-import java.sql.Timestamp;
+import java.time.Instant;
 
 public final class UserIdentity {
   /** Full name of the user. */
@@ -27,7 +27,7 @@
   private String username;
 
   /** Time (in UTC) when the identity was constructed. */
-  private Timestamp when;
+  private Instant when;
 
   /** Offset from UTC */
   private int tz;
@@ -55,11 +55,11 @@
     return username;
   }
 
-  public Timestamp getDate() {
+  public Instant getDate() {
     return when;
   }
 
-  public void setDate(Timestamp d) {
+  public void setDate(Instant d) {
     when = d;
   }
 
diff --git a/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
index eb2a381..edf921e 100644
--- a/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeMessageProtoConverter.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 @Immutable
@@ -44,9 +44,9 @@
     if (author != null) {
       builder.setAuthorId(accountIdConverter.toProto(author));
     }
-    Timestamp writtenOn = changeMessage.getWrittenOn();
+    Instant writtenOn = changeMessage.getWrittenOn();
     if (writtenOn != null) {
-      builder.setWrittenOn(writtenOn.getTime());
+      builder.setWrittenOn(writtenOn.toEpochMilli());
     }
     // Build proto with template representation of the message. Templates are parsed when message is
     // extracted from cache.
@@ -78,7 +78,7 @@
         proto.hasKey() ? changeMessageKeyConverter.fromProto(proto.getKey()) : null;
     Account.Id author =
         proto.hasAuthorId() ? accountIdConverter.fromProto(proto.getAuthorId()) : null;
-    Timestamp writtenOn = proto.hasWrittenOn() ? new Timestamp(proto.getWrittenOn()) : null;
+    Instant writtenOn = proto.hasWrittenOn() ? Instant.ofEpochMilli(proto.getWrittenOn()) : null;
     PatchSet.Id patchSetId =
         proto.hasPatchset() ? patchSetIdConverter.fromProto(proto.getPatchset()) : null;
     // Only template representation of the message is stored in entity. Templates should be replaced
diff --git a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
index 689b4aa..4903364 100644
--- a/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/ChangeProtoConverter.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 @Immutable
 public enum ChangeProtoConverter implements ProtoConverter<Entities.Change, Change> {
@@ -44,8 +44,8 @@
         Entities.Change.newBuilder()
             .setChangeId(changeIdConverter.toProto(change.getId()))
             .setChangeKey(changeKeyConverter.toProto(change.getKey()))
-            .setCreatedOn(change.getCreatedOn().getTime())
-            .setLastUpdatedOn(change.getLastUpdatedOn().getTime())
+            .setCreatedOn(change.getCreatedOn().toEpochMilli())
+            .setLastUpdatedOn(change.getLastUpdatedOn().toEpochMilli())
             .setOwnerAccountId(accountIdConverter.toProto(change.getOwner()))
             .setDest(branchNameConverter.toProto(change.getDest()))
             .setStatus(change.getStatus().getCode())
@@ -96,9 +96,9 @@
     BranchNameKey destination =
         proto.hasDest() ? branchNameConverter.fromProto(proto.getDest()) : null;
     Change change =
-        new Change(key, changeId, owner, destination, new Timestamp(proto.getCreatedOn()));
+        new Change(key, changeId, owner, destination, Instant.ofEpochMilli(proto.getCreatedOn()));
     if (proto.hasLastUpdatedOn()) {
-      change.setLastUpdatedOn(new Timestamp(proto.getLastUpdatedOn()));
+      change.setLastUpdatedOn(Instant.ofEpochMilli(proto.getLastUpdatedOn()));
     }
     Change.Status status = Change.Status.forCode((char) proto.getStatus());
     if (status != null) {
diff --git a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
index 134e25f..e8ef346 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverter.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 @Immutable
@@ -38,7 +38,7 @@
         Entities.PatchSetApproval.newBuilder()
             .setKey(patchSetApprovalKeyProtoConverter.toProto(patchSetApproval.key()))
             .setValue(patchSetApproval.value())
-            .setGranted(patchSetApproval.granted().getTime())
+            .setGranted(patchSetApproval.granted().toEpochMilli())
             .setPostSubmit(patchSetApproval.postSubmit())
             .setCopied(patchSetApproval.copied());
 
@@ -62,7 +62,7 @@
         PatchSetApproval.builder()
             .key(patchSetApprovalKeyProtoConverter.fromProto(proto.getKey()))
             .value(proto.getValue())
-            .granted(new Timestamp(proto.getGranted()))
+            .granted(Instant.ofEpochMilli(proto.getGranted()))
             .postSubmit(proto.getPostSubmit())
             .copied(proto.getCopied());
     if (proto.hasUuid()) {
diff --git a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
index 13a6e71..210972d 100644
--- a/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
+++ b/java/com/google/gerrit/entities/converter/PatchSetProtoConverter.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.proto.Entities;
 import com.google.protobuf.Parser;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -42,7 +42,7 @@
             .setId(patchSetIdConverter.toProto(patchSet.id()))
             .setCommitId(objectIdConverter.toProto(patchSet.commitId()))
             .setUploaderAccountId(accountIdConverter.toProto(patchSet.uploader()))
-            .setCreatedOn(patchSet.createdOn().getTime());
+            .setCreatedOn(patchSet.createdOn().toEpochMilli());
     List<String> groups = patchSet.groups();
     if (!groups.isEmpty()) {
       builder.setGroups(PatchSet.joinGroups(groups));
@@ -84,7 +84,8 @@
             proto.hasUploaderAccountId()
                 ? accountIdConverter.fromProto(proto.getUploaderAccountId())
                 : Account.id(0))
-        .createdOn(proto.hasCreatedOn() ? new Timestamp(proto.getCreatedOn()) : new Timestamp(0));
+        .createdOn(
+            proto.hasCreatedOn() ? Instant.ofEpochMilli(proto.getCreatedOn()) : Instant.EPOCH);
 
     return builder.build();
   }
diff --git a/java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java b/java/com/google/gerrit/exceptions/MergeUpdateException.java
similarity index 64%
rename from java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java
rename to java/com/google/gerrit/exceptions/MergeUpdateException.java
index 452192c..b60ca57 100644
--- a/java/com/google/gerrit/exceptions/InternalServerWithUserMessageException.java
+++ b/java/com/google/gerrit/exceptions/MergeUpdateException.java
@@ -14,10 +14,15 @@
 
 package com.google.gerrit.exceptions;
 
-public class InternalServerWithUserMessageException extends RuntimeException {
+/**
+ * An exception used for changes that fail to merge. This exception has a user visible message
+ * unlike other {@link RuntimeException}s, because this is our way to improve the UX when
+ * submission/merges fail.
+ */
+public class MergeUpdateException extends RuntimeException {
   private static final long serialVersionUID = 1L;
 
-  public InternalServerWithUserMessageException(String msg, Throwable cause) {
-    super(msg, cause);
+  public MergeUpdateException(String userVisibleMessage, Throwable cause) {
+    super(userVisibleMessage, cause);
   }
 }
diff --git a/java/com/google/gerrit/extensions/api/projects/TagInfo.java b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
index a6269fe..61ea518 100644
--- a/java/com/google/gerrit/extensions/api/projects/TagInfo.java
+++ b/java/com/google/gerrit/extensions/api/projects/TagInfo.java
@@ -14,16 +14,22 @@
 
 package com.google.gerrit.extensions.api.projects;
 
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.extensions.common.GitPerson;
 import com.google.gerrit.extensions.common.WebLinkInfo;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 
 public class TagInfo extends RefInfo {
   public String object;
   public String message;
   public GitPerson tagger;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp created;
+
   public List<WebLinkInfo> webLinks;
 
   public TagInfo(
@@ -39,8 +45,22 @@
     this.created = created;
   }
 
+  @SuppressWarnings("JdkObsolete")
+  public TagInfo(
+      String ref,
+      String revision,
+      Boolean canDelete,
+      List<WebLinkInfo> webLinks,
+      @Nullable Instant created) {
+    this.ref = ref;
+    this.revision = revision;
+    this.canDelete = canDelete;
+    this.webLinks = webLinks;
+    this.created = created != null ? Timestamp.from(created) : null;
+  }
+
   public TagInfo(String ref, String revision, Boolean canDelete, List<WebLinkInfo> webLinks) {
-    this(ref, revision, canDelete, webLinks, null);
+    this(ref, revision, canDelete, webLinks, (Instant) null);
   }
 
   public TagInfo(
@@ -66,8 +86,24 @@
       String message,
       GitPerson tagger,
       Boolean canDelete,
+      List<WebLinkInfo> webLinks,
+      Instant created) {
+    this(ref, revision, canDelete, webLinks, created);
+    this.object = object;
+    this.message = message;
+    this.tagger = tagger;
+    this.webLinks = webLinks;
+  }
+
+  public TagInfo(
+      String ref,
+      String revision,
+      String object,
+      String message,
+      GitPerson tagger,
+      Boolean canDelete,
       List<WebLinkInfo> webLinks) {
-    this(ref, revision, object, message, tagger, canDelete, webLinks, null);
+    this(ref, revision, object, message, tagger, canDelete, webLinks, (Instant) null);
     this.object = object;
     this.message = message;
     this.tagger = tagger;
diff --git a/java/com/google/gerrit/extensions/client/Comment.java b/java/com/google/gerrit/extensions/client/Comment.java
index faf14b1..b8843d3 100644
--- a/java/com/google/gerrit/extensions/client/Comment.java
+++ b/java/com/google/gerrit/extensions/client/Comment.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.client;
 
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Comparator;
 import java.util.Objects;
 
@@ -35,7 +36,11 @@
 
   public Range range;
   public String inReplyTo;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp updated;
+
   public String message;
 
   /**
@@ -44,6 +49,20 @@
    */
   public String commitId;
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public Instant getUpdated() {
+    return updated.toInstant();
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setUpdated(Instant when) {
+    updated = Timestamp.from(when);
+  }
+
   public static class Range implements Comparable<Range> {
     private static final Comparator<Range> RANGE_COMPARATOR =
         Comparator.<Range>comparingInt(range -> range.startLine)
diff --git a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
index a2aeab2..a76a7f9 100644
--- a/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountDetailInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common;
 
 import java.sql.Timestamp;
+import java.time.Instant;
 
 /**
  * Representation of a (detailed) account in the REST API.
@@ -27,9 +28,18 @@
  */
 public class AccountDetailInfo extends AccountInfo {
   /** The timestamp of when the account was registered. */
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp registeredOn;
 
   public AccountDetailInfo(Integer id) {
     super(id);
   }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setRegisteredOn(Instant registeredOn) {
+    this.registeredOn = Timestamp.from(registeredOn);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/ApprovalInfo.java b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
index a4e0baa..4519add 100644
--- a/java/com/google/gerrit/extensions/common/ApprovalInfo.java
+++ b/java/com/google/gerrit/extensions/common/ApprovalInfo.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -43,6 +44,8 @@
   public Integer value;
 
   /** The time and date describing when the approval was made. */
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp date;
 
   /** Whether this vote was made after the change was submitted. */
@@ -62,10 +65,10 @@
 
   public ApprovalInfo(
       Integer id,
-      Integer value,
+      @Nullable Integer value,
       @Nullable VotingRangeInfo permittedVotingRange,
       @Nullable String tag,
-      Timestamp date) {
+      @Nullable Timestamp date) {
     super(id);
     this.value = value;
     this.permittedVotingRange = permittedVotingRange;
@@ -73,6 +76,28 @@
     this.tag = tag;
   }
 
+  public ApprovalInfo(
+      Integer id,
+      @Nullable Integer value,
+      @Nullable VotingRangeInfo permittedVotingRange,
+      @Nullable String tag,
+      @Nullable Instant date) {
+    super(id);
+    this.value = value;
+    this.permittedVotingRange = permittedVotingRange;
+    this.tag = tag;
+    if (date != null) {
+      setDate(date);
+    }
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setDate(Instant date) {
+    this.date = Timestamp.from(date);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (o instanceof ApprovalInfo) {
diff --git a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
index d34ba6d..81dbc88 100644
--- a/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
+++ b/java/com/google/gerrit/extensions/common/AttentionSetInfo.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 /**
@@ -28,8 +29,12 @@
 public class AttentionSetInfo {
   /** The user included in the attention set. */
   public AccountInfo account;
+
   /** The timestamp of the last update. */
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp lastUpdate;
+
   /** The human readable reason why the user was added. */
   public String reason;
 
@@ -51,6 +56,17 @@
     this.reasonAccount = reasonAccount;
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public AttentionSetInfo(
+      AccountInfo account, Instant lastUpdate, String reason, @Nullable AccountInfo reasonAccount) {
+    this.account = account;
+    this.lastUpdate = Timestamp.from(lastUpdate);
+    this.reason = reason;
+    this.reasonAccount = reasonAccount;
+  }
+
   protected AttentionSetInfo() {}
 
   @Override
diff --git a/java/com/google/gerrit/extensions/common/ChangeInfo.java b/java/com/google/gerrit/extensions/common/ChangeInfo.java
index 55e4670..40ae2ec 100644
--- a/java/com/google/gerrit/extensions/common/ChangeInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeInfo.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.extensions.client.ReviewerState;
 import com.google.gerrit.extensions.client.SubmitType;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -53,9 +54,13 @@
   public String changeId;
   public String subject;
   public ChangeStatus status;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp created;
   public Timestamp updated;
   public Timestamp submitted;
+
   public AccountInfo submitter;
   public Boolean starred;
   public Collection<String> stars;
@@ -126,4 +131,47 @@
   public ChangeInfo(Map<String, RevisionInfo> revisions) {
     this.revisions = ImmutableMap.copyOf(revisions);
   }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public Instant getCreated() {
+    return created.toInstant();
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setCreated(Instant when) {
+    created = Timestamp.from(when);
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public Instant getUpdated() {
+    return updated.toInstant();
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setUpdated(Instant when) {
+    updated = Timestamp.from(when);
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public Instant getSubmitted() {
+    return submitted.toInstant();
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setSubmitted(Instant when, AccountInfo who) {
+    submitted = Timestamp.from(when);
+    submitter = who;
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
index e7e46c1..51fe57c 100644
--- a/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
+++ b/java/com/google/gerrit/extensions/common/ChangeMessageInfo.java
@@ -16,6 +16,7 @@
 
 import com.google.common.collect.Iterables;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Objects;
 
@@ -25,7 +26,11 @@
   public String tag;
   public AccountInfo author;
   public AccountInfo realAuthor;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp date;
+
   public String message;
   public Collection<AccountInfo> accountsInMessage;
   public Integer _revisionNumber;
@@ -36,6 +41,13 @@
     this.message = message;
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setDate(Instant when) {
+    date = Timestamp.from(when);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (o instanceof ChangeMessageInfo) {
diff --git a/java/com/google/gerrit/extensions/common/GitPerson.java b/java/com/google/gerrit/extensions/common/GitPerson.java
index 8ed919e..df3e488 100644
--- a/java/com/google/gerrit/extensions/common/GitPerson.java
+++ b/java/com/google/gerrit/extensions/common/GitPerson.java
@@ -15,14 +15,26 @@
 package com.google.gerrit.extensions.common;
 
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 public class GitPerson {
   public String name;
   public String email;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp date;
+
   public int tz;
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setDate(Instant when) {
+    date = Timestamp.from(when);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (!(o instanceof GitPerson)) {
diff --git a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
index 711337a..9a13713 100644
--- a/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupAuditEventInfo.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.extensions.common;
 
+import com.google.gerrit.common.Nullable;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 public abstract class GroupAuditEventInfo {
@@ -27,25 +29,62 @@
 
   public Type type;
   public AccountInfo user;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp date;
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
   public static UserMemberAuditEventInfo createAddUserEvent(
       AccountInfo user, Timestamp date, AccountInfo member) {
-    return new UserMemberAuditEventInfo(Type.ADD_USER, user, Optional.of(date), member);
+    return new UserMemberAuditEventInfo(Type.ADD_USER, user, date.toInstant(), member);
+  }
+
+  public static UserMemberAuditEventInfo createAddUserEvent(
+      AccountInfo user, Instant date, AccountInfo member) {
+    return new UserMemberAuditEventInfo(Type.ADD_USER, user, date, member);
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public static UserMemberAuditEventInfo createRemoveUserEvent(
+      AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+    return new UserMemberAuditEventInfo(
+        Type.REMOVE_USER, user, date.map(Timestamp::toInstant).orElse(null), member);
   }
 
   public static UserMemberAuditEventInfo createRemoveUserEvent(
-      AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+      AccountInfo user, @Nullable Instant date, AccountInfo member) {
     return new UserMemberAuditEventInfo(Type.REMOVE_USER, user, date, member);
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
   public static GroupMemberAuditEventInfo createAddGroupEvent(
       AccountInfo user, Timestamp date, GroupInfo member) {
-    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, Optional.of(date), member);
+    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date.toInstant(), member);
+  }
+
+  public static GroupMemberAuditEventInfo createAddGroupEvent(
+      AccountInfo user, Instant date, GroupInfo member) {
+    return new GroupMemberAuditEventInfo(Type.ADD_GROUP, user, date, member);
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public static GroupMemberAuditEventInfo createRemoveGroupEvent(
+      AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+    return new GroupMemberAuditEventInfo(
+        Type.REMOVE_GROUP, user, date.map(Timestamp::toInstant).orElse(null), member);
   }
 
   public static GroupMemberAuditEventInfo createRemoveGroupEvent(
-      AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+      AccountInfo user, @Nullable Instant date, GroupInfo member) {
     return new GroupMemberAuditEventInfo(Type.REMOVE_GROUP, user, date, member);
   }
 
@@ -55,11 +94,20 @@
     this.date = date.orElse(null);
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  protected GroupAuditEventInfo(Type type, AccountInfo user, @Nullable Instant date) {
+    this.type = type;
+    this.user = user;
+    this.date = date != null ? Timestamp.from(date) : null;
+  }
+
   public static class UserMemberAuditEventInfo extends GroupAuditEventInfo {
     public AccountInfo member;
 
     private UserMemberAuditEventInfo(
-        Type type, AccountInfo user, Optional<Timestamp> date, AccountInfo member) {
+        Type type, AccountInfo user, @Nullable Instant date, AccountInfo member) {
       super(type, user, date);
       this.member = member;
     }
@@ -69,7 +117,7 @@
     public GroupInfo member;
 
     private GroupMemberAuditEventInfo(
-        Type type, AccountInfo user, Optional<Timestamp> date, GroupInfo member) {
+        Type type, AccountInfo user, @Nullable Instant date, GroupInfo member) {
       super(type, user, date);
       this.member = member;
     }
diff --git a/java/com/google/gerrit/extensions/common/GroupInfo.java b/java/com/google/gerrit/extensions/common/GroupInfo.java
index b21475c..edbaa01 100644
--- a/java/com/google/gerrit/extensions/common/GroupInfo.java
+++ b/java/com/google/gerrit/extensions/common/GroupInfo.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.extensions.common;
 
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 
 public class GroupInfo extends GroupBaseInfo {
@@ -26,10 +27,28 @@
   public Integer groupId;
   public String owner;
   public String ownerId;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp createdOn;
+
   public Boolean _moreGroups;
 
   // These fields are only supplied for internal groups, and only if requested.
   public List<AccountInfo> members;
   public List<GroupInfo> includes;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public Instant getCreatedOn() {
+    return createdOn.toInstant();
+  }
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setCreatedOn(Instant when) {
+    createdOn = Timestamp.from(when);
+  }
 }
diff --git a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
index 37e1ceb..36682f6 100644
--- a/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
+++ b/java/com/google/gerrit/extensions/common/ReviewerUpdateInfo.java
@@ -16,14 +16,31 @@
 
 import com.google.gerrit.extensions.client.ReviewerState;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 
 public class ReviewerUpdateInfo {
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp updated;
+
   public AccountInfo updatedBy;
   public AccountInfo reviewer;
   public ReviewerState state;
 
+  public ReviewerUpdateInfo() {}
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public ReviewerUpdateInfo(
+      Instant updated, AccountInfo updatedBy, AccountInfo reviewer, ReviewerState state) {
+    this.updated = Timestamp.from(updated);
+    this.updatedBy = updatedBy;
+    this.reviewer = reviewer;
+    this.state = state;
+  }
+
   @Override
   public boolean equals(Object o) {
     if (o instanceof ReviewerUpdateInfo) {
diff --git a/java/com/google/gerrit/extensions/common/RevisionInfo.java b/java/com/google/gerrit/extensions/common/RevisionInfo.java
index f710ab7..7c52c8c 100644
--- a/java/com/google/gerrit/extensions/common/RevisionInfo.java
+++ b/java/com/google/gerrit/extensions/common/RevisionInfo.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.extensions.client.ChangeKind;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Map;
 import java.util.Objects;
 
@@ -25,7 +26,11 @@
   public transient boolean isCurrent;
   public ChangeKind kind;
   public int _number;
+
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
   public Timestamp created;
+
   public AccountInfo uploader;
   public String ref;
   public Map<String, FetchInfo> fetch;
@@ -51,6 +56,13 @@
     this.uploader = uploader;
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
+  public void setCreated(Instant date) {
+    this.created = Timestamp.from(date);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (o instanceof RevisionInfo) {
diff --git a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
index 0f6af21..cb9d855 100644
--- a/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
+++ b/java/com/google/gerrit/extensions/common/testing/GitPersonSubject.java
@@ -70,6 +70,11 @@
     tz().isEqualTo(other.tz);
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public void matches(PersonIdent ident) {
     isNotNull();
     name().isEqualTo(ident.getName());
diff --git a/java/com/google/gerrit/extensions/events/ChangeEvent.java b/java/com/google/gerrit/extensions/events/ChangeEvent.java
index def75b7..6542d8e 100644
--- a/java/com/google/gerrit/extensions/events/ChangeEvent.java
+++ b/java/com/google/gerrit/extensions/events/ChangeEvent.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Interface to be extended by Events with a Change. */
 public interface ChangeEvent extends GerritEvent {
@@ -29,5 +29,5 @@
 
   AccountInfo getWho();
 
-  Timestamp getWhen();
+  Instant getWhen();
 }
diff --git a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
index e0c921d..bcc8631 100644
--- a/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
+++ b/java/com/google/gerrit/gpg/server/DeleteGpgKey.java
@@ -95,7 +95,7 @@
 
       CommitBuilder cb = new CommitBuilder();
       PersonIdent committer = serverIdent.get();
-      cb.setAuthor(rsrc.getUser().newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
+      cb.setAuthor(rsrc.getUser().newCommitterIdent(committer));
       cb.setCommitter(committer);
       cb.setMessage("Delete public key " + keyIdToString(key.getKeyID()));
 
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
index 5dc6d01..3341806 100644
--- a/java/com/google/gerrit/gpg/server/PostGpgKeys.java
+++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java
@@ -250,7 +250,7 @@
       }
       CommitBuilder cb = new CommitBuilder();
       PersonIdent committer = serverIdent.get();
-      cb.setAuthor(user.newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
+      cb.setAuthor(user.newCommitterIdent(committer));
       cb.setCommitter(committer);
 
       RefUpdate.Result saveResult = store.save(cb);
diff --git a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
index 58630dd..36e9e52 100644
--- a/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
+++ b/java/com/google/gerrit/index/testing/AbstractFakeIndex.java
@@ -47,7 +47,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
@@ -205,7 +205,7 @@
       Comparator<ChangeData> lastUpdated =
           Comparator.comparing(cd -> cd.change().getLastUpdatedOn());
       Comparator<ChangeData> merged =
-          Comparator.comparing(cd -> cd.getMergedOn().orElse(new Timestamp(0)));
+          Comparator.comparing(cd -> cd.getMergedOn().orElse(Instant.EPOCH));
       Comparator<ChangeData> id = Comparator.comparing(cd -> cd.getId().get());
       return lastUpdated.thenComparing(merged).thenComparing(id).reversed();
     }
diff --git a/java/com/google/gerrit/pgm/init/AccountsOnInit.java b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
index d9e3a6a..cbfd714 100644
--- a/java/com/google/gerrit/pgm/init/AccountsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/AccountsOnInit.java
@@ -30,6 +30,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Date;
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheEditor;
 import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
@@ -60,12 +61,16 @@
     this.allUsers = allUsers.get();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public Account insert(Account.Builder account) throws IOException {
     File path = getPath();
     try (Repository repo = new FileRepository(path);
         ObjectInserter oi = repo.newObjectInserter()) {
       PersonIdent ident =
-          new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), account.registeredOn());
+          new PersonIdent(
+              new GerritPersonIdentProvider(flags.cfg).get(), Date.from(account.registeredOn()));
 
       Config accountConfig = new Config();
       AccountProperties.writeToAccountConfig(
diff --git a/java/com/google/gerrit/pgm/init/GroupsOnInit.java b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
index 2f12abb..020705e 100644
--- a/java/com/google/gerrit/pgm/init/GroupsOnInit.java
+++ b/java/com/google/gerrit/pgm/init/GroupsOnInit.java
@@ -41,6 +41,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.stream.Stream;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.internal.storage.file.FileRepository;
@@ -165,10 +166,14 @@
     return AuditLogFormatter.createBackedBy(ImmutableSet.of(account), ImmutableSet.of(), serverId);
   }
 
-  private void commit(Repository repository, GroupConfig groupConfig, Timestamp groupCreatedOn)
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  private void commit(Repository repository, GroupConfig groupConfig, Instant groupCreatedOn)
       throws IOException {
     PersonIdent personIdent =
-        new PersonIdent(new GerritPersonIdentProvider(flags.cfg).get(), groupCreatedOn);
+        new PersonIdent(
+            new GerritPersonIdentProvider(flags.cfg).get(), Timestamp.from(groupCreatedOn));
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate(repository, personIdent)) {
       groupConfig.commit(metaDataUpdate);
     }
diff --git a/java/com/google/gerrit/pgm/init/InitAdminUser.java b/java/com/google/gerrit/pgm/init/InitAdminUser.java
index d6a0133..4e854b5 100644
--- a/java/com/google/gerrit/pgm/init/InitAdminUser.java
+++ b/java/com/google/gerrit/pgm/init/InitAdminUser.java
@@ -120,7 +120,7 @@
 
         Account persistedAccount =
             accounts.insert(
-                Account.builder(id, TimeUtil.nowTs()).setFullName(name).setPreferredEmail(email));
+                Account.builder(id, TimeUtil.now()).setFullName(name).setPreferredEmail(email));
         // Only two groups should exist at this point in time and hence iterating over all of them
         // is cheap.
         Optional<GroupReference> adminGroupReference =
diff --git a/java/com/google/gerrit/server/AssigneeStatusUpdate.java b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
index 3d6242b..812aad1 100644
--- a/java/com/google/gerrit/server/AssigneeStatusUpdate.java
+++ b/java/com/google/gerrit/server/AssigneeStatusUpdate.java
@@ -16,18 +16,18 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.Account;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 
 /** Change to an assignee's status. */
 @AutoValue
 public abstract class AssigneeStatusUpdate {
   public static AssigneeStatusUpdate create(
-      Timestamp ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
+      Instant ts, Account.Id updatedBy, Optional<Account.Id> currentAssignee) {
     return new AutoValue_AssigneeStatusUpdate(ts, updatedBy, currentAssignee);
   }
 
-  public abstract Timestamp date();
+  public abstract Instant date();
 
   public abstract Account.Id updatedBy();
 
diff --git a/java/com/google/gerrit/server/ChangeMessagesUtil.java b/java/com/google/gerrit/server/ChangeMessagesUtil.java
index 8366b09..81cff6e 100644
--- a/java/com/google/gerrit/server/ChangeMessagesUtil.java
+++ b/java/com/google/gerrit/server/ChangeMessagesUtil.java
@@ -145,7 +145,7 @@
     ChangeMessageInfo cmi = new ChangeMessageInfo();
     cmi.id = message.getKey().uuid();
     cmi.author = accountLoader.get(message.getAuthor());
-    cmi.date = message.getWrittenOn();
+    cmi.setDate(message.getWrittenOn());
     cmi.message = message.getMessage();
     cmi.tag = message.getTag();
     cmi._revisionNumber = patchNum != null ? patchNum.get() : null;
diff --git a/java/com/google/gerrit/server/CmdLineParserModule.java b/java/com/google/gerrit/server/CmdLineParserModule.java
index d943889..be6b4cd8 100644
--- a/java/com/google/gerrit/server/CmdLineParserModule.java
+++ b/java/com/google/gerrit/server/CmdLineParserModule.java
@@ -23,17 +23,17 @@
 import com.google.gerrit.server.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.server.args4j.AccountIdHandler;
 import com.google.gerrit.server.args4j.ChangeIdHandler;
+import com.google.gerrit.server.args4j.InstantHandler;
 import com.google.gerrit.server.args4j.ObjectIdHandler;
 import com.google.gerrit.server.args4j.PatchSetIdHandler;
 import com.google.gerrit.server.args4j.ProjectHandler;
 import com.google.gerrit.server.args4j.SocketAddressHandler;
-import com.google.gerrit.server.args4j.TimestampHandler;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
 import com.google.gerrit.util.cli.OptionHandlers;
 import java.net.SocketAddress;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.spi.OptionHandler;
 
@@ -49,11 +49,11 @@
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
     registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
     registerOptionHandler(Change.Id.class, ChangeIdHandler.class);
+    registerOptionHandler(Instant.class, InstantHandler.class);
     registerOptionHandler(ObjectId.class, ObjectIdHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectState.class, ProjectHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
-    registerOptionHandler(Timestamp.class, TimestampHandler.class);
   }
 
   private <T> void registerOptionHandler(Class<T> type, Class<? extends OptionHandler<T>> impl) {
diff --git a/java/com/google/gerrit/server/CommentsUtil.java b/java/com/google/gerrit/server/CommentsUtil.java
index 86092c4..8198ce4 100644
--- a/java/com/google/gerrit/server/CommentsUtil.java
+++ b/java/com/google/gerrit/server/CommentsUtil.java
@@ -50,7 +50,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -133,7 +133,7 @@
   public HumanComment newHumanComment(
       ChangeNotes changeNotes,
       CurrentUser currentUser,
-      Timestamp when,
+      Instant when,
       String path,
       PatchSet.Id psId,
       short side,
@@ -305,7 +305,7 @@
   }
 
   private static boolean isAfter(CommentInfo c, ChangeMessage cm) {
-    return c.updated.after(cm.getWrittenOn());
+    return c.getUpdated().isAfter(cm.getWrittenOn());
   }
 
   /**
diff --git a/java/com/google/gerrit/server/CommonConverters.java b/java/com/google/gerrit/server/CommonConverters.java
index 2b48169..e7fd1c5 100644
--- a/java/com/google/gerrit/server/CommonConverters.java
+++ b/java/com/google/gerrit/server/CommonConverters.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server;
 
 import com.google.gerrit.extensions.common.GitPerson;
-import java.sql.Timestamp;
 import org.eclipse.jgit.lib.PersonIdent;
 
 /**
@@ -26,11 +25,14 @@
  * static utility methods.
  */
 public class CommonConverters {
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public static GitPerson toGitPerson(PersonIdent ident) {
     GitPerson result = new GitPerson();
     result.name = ident.getName();
     result.email = ident.getEmailAddress();
-    result.date = new Timestamp(ident.getWhen().getTime());
+    result.setDate(ident.getWhen().toInstant());
     result.tz = ident.getTimeZoneOffset();
     return result;
   }
diff --git a/java/com/google/gerrit/server/ExceptionHookImpl.java b/java/com/google/gerrit/server/ExceptionHookImpl.java
index 3986842..781f196 100644
--- a/java/com/google/gerrit/server/ExceptionHookImpl.java
+++ b/java/com/google/gerrit/server/ExceptionHookImpl.java
@@ -18,7 +18,7 @@
 import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gerrit.common.Nullable;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
 import com.google.gerrit.git.LockFailureException;
 import com.google.gerrit.server.project.ProjectConfig;
 import java.util.Optional;
@@ -74,7 +74,7 @@
               + "\n"
               + CONTACT_PROJECT_OWNER_USER_MESSAGE);
     }
-    if (throwable instanceof InternalServerWithUserMessageException) {
+    if (throwable instanceof MergeUpdateException) {
       return ImmutableList.of(throwable.getMessage());
     }
     return ImmutableList.of();
diff --git a/java/com/google/gerrit/server/IdentifiedUser.java b/java/com/google/gerrit/server/IdentifiedUser.java
index eb3e324..122e18d 100644
--- a/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/java/com/google/gerrit/server/IdentifiedUser.java
@@ -49,7 +49,7 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.Set;
 import java.util.TimeZone;
@@ -427,10 +427,10 @@
   }
 
   public PersonIdent newRefLogIdent() {
-    return newRefLogIdent(new Date(), TimeZone.getDefault());
+    return newRefLogIdent(Instant.now(), TimeZone.getDefault());
   }
 
-  public PersonIdent newRefLogIdent(Date when, TimeZone tz) {
+  public PersonIdent newRefLogIdent(Instant when, TimeZone tz) {
     final Account ua = getAccount();
 
     String name = ua.fullName();
@@ -450,14 +450,22 @@
               ? constructMailAddress(ua, "unknown")
               : ua.preferredEmail();
     }
-    return new PersonIdent(name, user, when, tz);
+
+    return newPersonIdent(name, user, when, tz);
   }
 
   private String constructMailAddress(Account ua, String host) {
     return getUserName().orElse("") + "|account-" + ua.id().toString() + "@" + host;
   }
 
-  public PersonIdent newCommitterIdent(Date when, TimeZone tz) {
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  public PersonIdent newCommitterIdent(PersonIdent ident) {
+    return newCommitterIdent(ident.getWhen().toInstant(), ident.getTimeZone());
+  }
+
+  public PersonIdent newCommitterIdent(Instant when, TimeZone tz) {
     final Account ua = getAccount();
     String name = ua.fullName();
     String email = ua.preferredEmail();
@@ -492,7 +500,7 @@
       }
     }
 
-    return new PersonIdent(name, email, when, tz);
+    return newPersonIdent(name, email, when, tz);
   }
 
   @Override
@@ -560,4 +568,19 @@
     }
     return host;
   }
+
+  /**
+   * Create a {@link PersonIdent} from an {@code Instant} and a {@link TimeZone}.
+   *
+   * <p>We use the {@link PersonIdent#PersonIdent(String, String, long, int)} constructor to avoid
+   * doing a conversion to {@code java.util.Date} here. For the {@code int aTZ} argument, which is
+   * the time zone, we do the same computation as in {@link PersonIdent#PersonIdent(String, String,
+   * java.util.Date, TimeZone)} (just instead of getting the epoch millis from {@code
+   * java.util.Date} we get them from {@link Instant}).
+   */
+  // TODO(issue-15517): Drop this method once JGit's PersonIdent class supports Instants
+  private static PersonIdent newPersonIdent(String name, String email, Instant when, TimeZone tz) {
+    return new PersonIdent(
+        name, email, when.toEpochMilli(), tz.getOffset(when.toEpochMilli()) / (60 * 1000));
+  }
 }
diff --git a/java/com/google/gerrit/server/PatchSetUtil.java b/java/com/google/gerrit/server/PatchSetUtil.java
index 326ddf4..3d449b7 100644
--- a/java/com/google/gerrit/server/PatchSetUtil.java
+++ b/java/com/google/gerrit/server/PatchSetUtil.java
@@ -39,7 +39,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -106,7 +105,7 @@
         .id(psId)
         .commitId(commit)
         .uploader(update.getAccountId())
-        .createdOn(new Timestamp(update.getWhen().getTime()))
+        .createdOn(update.getWhen())
         .groups(groups)
         .pushCertificate(Optional.ofNullable(pushCertificate))
         .description(Optional.ofNullable(description))
diff --git a/java/com/google/gerrit/server/PublishCommentUtil.java b/java/com/google/gerrit/server/PublishCommentUtil.java
index 4d19dd0..de5f023 100644
--- a/java/com/google/gerrit/server/PublishCommentUtil.java
+++ b/java/com/google/gerrit/server/PublishCommentUtil.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.update.ChangeContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.sql.Timestamp;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Map;
@@ -90,7 +91,7 @@
             draftComment, psIdOfDraftComment, notes.getProjectName());
         continue;
       }
-      draftComment.writtenOn = ctx.getWhen();
+      draftComment.writtenOn = Timestamp.from(ctx.getWhen());
       draftComment.tag = tag;
       // Draft may have been created by a different real user; copy the current real user. (Only
       // applies to X-Gerrit-RunAs, since modifying drafts via on_behalf_of is not allowed.)
diff --git a/java/com/google/gerrit/server/ReviewerByEmailSet.java b/java/com/google/gerrit/server/ReviewerByEmailSet.java
index 4a317c3..c6ba7b5 100644
--- a/java/com/google/gerrit/server/ReviewerByEmailSet.java
+++ b/java/com/google/gerrit/server/ReviewerByEmailSet.java
@@ -19,7 +19,7 @@
 import com.google.common.collect.Table;
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /**
  * Set of reviewers on a change that do not have a Gerrit account and were added by email instead.
@@ -30,8 +30,7 @@
 public class ReviewerByEmailSet {
   private static final ReviewerByEmailSet EMPTY = new ReviewerByEmailSet(ImmutableTable.of());
 
-  public static ReviewerByEmailSet fromTable(
-      Table<ReviewerStateInternal, Address, Timestamp> table) {
+  public static ReviewerByEmailSet fromTable(Table<ReviewerStateInternal, Address, Instant> table) {
     return new ReviewerByEmailSet(table);
   }
 
@@ -39,10 +38,10 @@
     return EMPTY;
   }
 
-  private final ImmutableTable<ReviewerStateInternal, Address, Timestamp> table;
+  private final ImmutableTable<ReviewerStateInternal, Address, Instant> table;
   private ImmutableSet<Address> users;
 
-  private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Timestamp> table) {
+  private ReviewerByEmailSet(Table<ReviewerStateInternal, Address, Instant> table) {
     this.table = ImmutableTable.copyOf(table);
   }
 
@@ -58,7 +57,7 @@
     return table.row(state).keySet();
   }
 
-  public ImmutableTable<ReviewerStateInternal, Address, Timestamp> asTable() {
+  public ImmutableTable<ReviewerStateInternal, Address, Instant> asTable() {
     return table;
   }
 
diff --git a/java/com/google/gerrit/server/ReviewerSet.java b/java/com/google/gerrit/server/ReviewerSet.java
index 0f6bf29..0ff68e0 100644
--- a/java/com/google/gerrit/server/ReviewerSet.java
+++ b/java/com/google/gerrit/server/ReviewerSet.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /**
  * Set of reviewers on a change.
@@ -38,7 +38,7 @@
 
   public static ReviewerSet fromApprovals(Iterable<PatchSetApproval> approvals) {
     PatchSetApproval first = null;
-    Table<ReviewerStateInternal, Account.Id, Timestamp> reviewers = HashBasedTable.create();
+    Table<ReviewerStateInternal, Account.Id, Instant> reviewers = HashBasedTable.create();
     for (PatchSetApproval psa : approvals) {
       if (first == null) {
         first = psa;
@@ -58,7 +58,7 @@
     return new ReviewerSet(reviewers);
   }
 
-  public static ReviewerSet fromTable(Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+  public static ReviewerSet fromTable(Table<ReviewerStateInternal, Account.Id, Instant> table) {
     return new ReviewerSet(table);
   }
 
@@ -66,10 +66,10 @@
     return EMPTY;
   }
 
-  private final ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp> table;
+  private final ImmutableTable<ReviewerStateInternal, Account.Id, Instant> table;
   private ImmutableSet<Account.Id> accounts;
 
-  private ReviewerSet(Table<ReviewerStateInternal, Account.Id, Timestamp> table) {
+  private ReviewerSet(Table<ReviewerStateInternal, Account.Id, Instant> table) {
     this.table = ImmutableTable.copyOf(table);
   }
 
@@ -85,7 +85,7 @@
     return table.row(state).keySet();
   }
 
-  public ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp> asTable() {
+  public ImmutableTable<ReviewerStateInternal, Account.Id, Instant> asTable() {
     return table;
   }
 
diff --git a/java/com/google/gerrit/server/ReviewerStatusUpdate.java b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
index 938d985..1e0aa43 100644
--- a/java/com/google/gerrit/server/ReviewerStatusUpdate.java
+++ b/java/com/google/gerrit/server/ReviewerStatusUpdate.java
@@ -17,17 +17,17 @@
 import com.google.auto.value.AutoValue;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.notedb.ReviewerStateInternal;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Change to a reviewer's status. */
 @AutoValue
 public abstract class ReviewerStatusUpdate {
   public static ReviewerStatusUpdate create(
-      Timestamp ts, Account.Id updatedBy, Account.Id reviewer, ReviewerStateInternal state) {
+      Instant ts, Account.Id updatedBy, Account.Id reviewer, ReviewerStateInternal state) {
     return new AutoValue_ReviewerStatusUpdate(ts, updatedBy, reviewer, state);
   }
 
-  public abstract Timestamp date();
+  public abstract Instant date();
 
   public abstract Account.Id updatedBy();
 
diff --git a/java/com/google/gerrit/server/account/AccountCacheImpl.java b/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 093af68..1d9150d 100644
--- a/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -153,7 +153,7 @@
   }
 
   private AccountState missing(Account.Id accountId) {
-    Account.Builder account = Account.builder(accountId, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(accountId, TimeUtil.now());
     account.setActive(false);
     return AccountState.forAccount(account.build());
   }
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 45f1f35..28e881e1 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -34,8 +34,9 @@
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -177,7 +178,7 @@
    * @throws DuplicateKeyException if the user branch already exists
    */
   public Account getNewAccount() throws DuplicateKeyException {
-    return getNewAccount(TimeUtil.nowTs());
+    return getNewAccount(TimeUtil.now());
   }
 
   /**
@@ -186,7 +187,7 @@
    * @return the new account
    * @throws DuplicateKeyException if the user branch already exists
    */
-  Account getNewAccount(Timestamp registeredOn) throws DuplicateKeyException {
+  Account getNewAccount(Instant registeredOn) throws DuplicateKeyException {
     checkLoaded();
     if (revision != null) {
       throw new DuplicateKeyException(String.format("account %s already exists", accountId));
@@ -216,7 +217,7 @@
       rw.reset();
       rw.markStart(revision);
       rw.sort(RevSort.REVERSE);
-      Timestamp registeredOn = new Timestamp(rw.next().getCommitTime() * 1000L);
+      Instant registeredOn = Instant.ofEpochMilli(rw.next().getCommitTime() * 1000L);
 
       Config accountConfig = readConfig(AccountProperties.ACCOUNT_CONFIG);
       loadedAccountProperties =
@@ -257,6 +258,9 @@
     return c;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Override
   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     checkLoaded();
@@ -274,9 +278,9 @@
         commit.setMessage("Create account\n");
       }
 
-      Timestamp registeredOn = loadedAccountProperties.get().getRegisteredOn();
-      commit.setAuthor(new PersonIdent(commit.getAuthor(), registeredOn));
-      commit.setCommitter(new PersonIdent(commit.getCommitter(), registeredOn));
+      Instant registeredOn = loadedAccountProperties.get().getRegisteredOn();
+      commit.setAuthor(new PersonIdent(commit.getAuthor(), Date.from(registeredOn)));
+      commit.setCommitter(new PersonIdent(commit.getCommitter(), Date.from(registeredOn)));
     }
 
     saveAccount();
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 9b7ca81..928d851 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -17,7 +17,7 @@
 import com.google.common.base.Strings;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -57,16 +57,13 @@
   public static final String KEY_STATUS = "status";
 
   private final Account.Id accountId;
-  private final Timestamp registeredOn;
+  private final Instant registeredOn;
   private final Config accountConfig;
   private @Nullable ObjectId metaId;
   private Account account;
 
   AccountProperties(
-      Account.Id accountId,
-      Timestamp registeredOn,
-      Config accountConfig,
-      @Nullable ObjectId metaId) {
+      Account.Id accountId, Instant registeredOn, Config accountConfig, @Nullable ObjectId metaId) {
     this.accountId = accountId;
     this.registeredOn = registeredOn;
     this.accountConfig = accountConfig;
@@ -80,7 +77,7 @@
     return account;
   }
 
-  public Timestamp getRegisteredOn() {
+  public Instant getRegisteredOn() {
     return registeredOn;
   }
 
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 3706304..0e643f8 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -49,7 +49,6 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
@@ -295,9 +294,7 @@
 
   private static PersonIdent createPersonIdent(
       PersonIdent serverIdent, Optional<IdentifiedUser> user) {
-    return user.isPresent()
-        ? user.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone())
-        : serverIdent;
+    return user.isPresent() ? user.get().newCommitterIdent(serverIdent) : serverIdent;
   }
 
   /**
@@ -322,6 +319,9 @@
    * @throws IOException if creating the user branch fails due to an IO error
    * @throws ConfigInvalidException if any of the account fields has an invalid value
    */
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public AccountState insert(String message, Account.Id accountId, ConfigureDeltaFromState init)
       throws IOException, ConfigInvalidException {
     return execute(
@@ -329,8 +329,7 @@
                 repo -> {
                   AccountConfig accountConfig = read(repo, accountId);
                   Account account =
-                      accountConfig.getNewAccount(
-                          new Timestamp(committerIdent.getWhen().getTime()));
+                      accountConfig.getNewAccount(committerIdent.getWhen().toInstant());
                   AccountState accountState = AccountState.forAccount(account);
                   AccountDelta.Builder deltaBuilder = AccountDelta.builder();
                   init.configure(accountState, deltaBuilder);
diff --git a/java/com/google/gerrit/server/account/CachedAccountDetails.java b/java/com/google/gerrit/server/account/CachedAccountDetails.java
index f23a766..2ab6174 100644
--- a/java/com/google/gerrit/server/account/CachedAccountDetails.java
+++ b/java/com/google/gerrit/server/account/CachedAccountDetails.java
@@ -30,7 +30,6 @@
 import com.google.gerrit.server.cache.serialize.CacheSerializer;
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.config.CachedPreferences;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.Map;
 import org.eclipse.jgit.lib.ObjectId;
@@ -107,7 +106,7 @@
       Cache.AccountProto.Builder accountProto =
           Cache.AccountProto.newBuilder()
               .setId(account.id().get())
-              .setRegisteredOn(account.registeredOn().toInstant().toEpochMilli())
+              .setRegisteredOn(account.registeredOn().toEpochMilli())
               .setInactive(account.inactive())
               .setFullName(Strings.nullToEmpty(account.fullName()))
               .setDisplayName(Strings.nullToEmpty(account.displayName()))
@@ -143,7 +142,7 @@
       Account account =
           Account.builder(
                   Account.id(proto.getAccount().getId()),
-                  Timestamp.from(Instant.ofEpochMilli(proto.getAccount().getRegisteredOn())))
+                  Instant.ofEpochMilli(proto.getAccount().getRegisteredOn()))
               .setFullName(Strings.emptyToNull(proto.getAccount().getFullName()))
               .setDisplayName(Strings.emptyToNull(proto.getAccount().getDisplayName()))
               .setPreferredEmail(Strings.emptyToNull(proto.getAccount().getPreferredEmail()))
diff --git a/java/com/google/gerrit/server/account/Emails.java b/java/com/google/gerrit/server/account/Emails.java
index 98d0d50..45f0844 100644
--- a/java/com/google/gerrit/server/account/Emails.java
+++ b/java/com/google/gerrit/server/account/Emails.java
@@ -29,7 +29,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -114,11 +113,14 @@
     return externalIds.byEmail(email).stream().map(ExternalId::accountId).collect(toImmutableSet());
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public UserIdentity toUserIdentity(PersonIdent who) throws IOException {
     UserIdentity u = new UserIdentity();
     u.setName(who.getName());
     u.setEmail(who.getEmailAddress());
-    u.setDate(new Timestamp(who.getWhen().getTime()));
+    u.setDate(who.getWhen().toInstant());
     u.setTimeZone(who.getTimeZoneOffset());
 
     // If only one account has access to this email address, select it
diff --git a/java/com/google/gerrit/server/approval/ApprovalsUtil.java b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
index c3921f8..8d227f7 100644
--- a/java/com/google/gerrit/server/approval/ApprovalsUtil.java
+++ b/java/com/google/gerrit/server/approval/ApprovalsUtil.java
@@ -53,10 +53,10 @@
 import com.google.gerrit.server.util.LabelVote;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -80,7 +80,7 @@
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
 
   public static PatchSetApproval.Builder newApproval(
-      PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Date when) {
+      PatchSet.Id psId, CurrentUser user, LabelId labelId, int value, Instant when) {
     PatchSetApproval.Builder b =
         PatchSetApproval.builder()
             .key(PatchSetApproval.key(psId, user.getAccountId(), labelId))
@@ -297,7 +297,7 @@
     }
     checkApprovals(approvals, permissionBackend.user(user).change(update.getNotes()));
     List<PatchSetApproval> cells = new ArrayList<>(approvals.size());
-    Date ts = update.getWhen();
+    Instant ts = update.getWhen();
     for (Map.Entry<String, Short> vote : approvals.entrySet()) {
       Optional<LabelType> lt = labelTypes.byLabel(vote.getKey());
       if (!lt.isPresent()) {
diff --git a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java
index f5110c3..e752ec5 100644
--- a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java
+++ b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGenerator.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.PatchSetApproval.UUID;
 import com.google.inject.ImplementedBy;
-import java.util.Date;
+import java.time.Instant;
 
 /**
  * Generator for {@link PatchSetApproval.UUID}.
@@ -34,5 +34,6 @@
    * Generates {@link PatchSetApproval.UUID} based on the properties of {@link PatchSetApproval}
    * that is being granted.
    */
-  UUID get(PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Date granted);
+  UUID get(
+      PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted);
 }
diff --git a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java
index 32aaf5ac..afa0384 100644
--- a/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java
+++ b/java/com/google/gerrit/server/approval/PatchSetApprovalUuidGeneratorImpl.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.PatchSetApproval.UUID;
 import com.google.inject.Singleton;
 import java.security.MessageDigest;
-import java.util.Date;
+import java.time.Instant;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -32,14 +32,14 @@
 public class PatchSetApprovalUuidGeneratorImpl implements PatchSetApprovalUuidGenerator {
   @Override
   public UUID get(
-      PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Date granted) {
+      PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted) {
     MessageDigest md = Constants.newMessageDigest();
     md.update(
         Constants.encode("patchSetId " + patchSetId.getCommaSeparatedChangeAndPatchSetId() + "\n"));
     md.update(Constants.encode("accountId " + accountId + "\n"));
     md.update(Constants.encode("label " + label + "\n"));
     md.update(Constants.encode("value " + value + "\n"));
-    md.update(Constants.encode("granted " + granted.getTime() + "\n"));
+    md.update(Constants.encode("granted " + granted.toEpochMilli() + "\n"));
     md.update(Constants.encode(String.valueOf(Math.random())));
     return PatchSetApproval.uuid(ObjectId.fromRaw(md.digest()).name());
   }
diff --git a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
index b67f8ee..f6527d2 100644
--- a/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
+++ b/java/com/google/gerrit/server/approval/testing/TestPatchSetApprovalUuidGenerator.java
@@ -5,7 +5,7 @@
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.PatchSetApproval.UUID;
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
-import java.util.Date;
+import java.time.Instant;
 import javax.inject.Singleton;
 
 /**
@@ -18,7 +18,7 @@
 
   @Override
   public UUID get(
-      PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Date granted) {
+      PatchSet.Id patchSetId, Account.Id accountId, String label, short value, Instant granted) {
     invocationCount++;
     return PatchSetApproval.uuid(
         String.format(
diff --git a/java/com/google/gerrit/server/args4j/TimestampHandler.java b/java/com/google/gerrit/server/args4j/InstantHandler.java
similarity index 73%
rename from java/com/google/gerrit/server/args4j/TimestampHandler.java
rename to java/com/google/gerrit/server/args4j/InstantHandler.java
index eddfbcd..bfca0f6 100644
--- a/java/com/google/gerrit/server/args4j/TimestampHandler.java
+++ b/java/com/google/gerrit/server/args4j/InstantHandler.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2014 The Android Open Source Project
+// Copyright (C) 2021 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -16,11 +16,10 @@
 
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
-import java.text.DateFormat;
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.TimeZone;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.OptionDef;
@@ -28,14 +27,14 @@
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
 
-public class TimestampHandler extends OptionHandler<Timestamp> {
+public class InstantHandler extends OptionHandler<Instant> {
   public static final String TIMESTAMP_FORMAT = "yyyyMMdd_HHmm";
 
   @Inject
-  public TimestampHandler(
+  public InstantHandler(
       @Assisted CmdLineParser parser,
       @Assisted OptionDef option,
-      @Assisted Setter<Timestamp> setter) {
+      @Assisted Setter<Instant> setter) {
     super(parser, option, setter);
   }
 
@@ -43,11 +42,12 @@
   public int parseArguments(Parameters params) throws CmdLineException {
     String timestamp = params.getParameter(0);
     try {
-      DateFormat fmt = new SimpleDateFormat(TIMESTAMP_FORMAT);
-      fmt.setTimeZone(TimeZone.getTimeZone("UTC"));
-      setter.addValue(new Timestamp(fmt.parse(timestamp).getTime()));
+      setter.addValue(
+          DateTimeFormatter.ofPattern(TIMESTAMP_FORMAT)
+              .withZone(ZoneId.of("UTC"))
+              .parse(timestamp, Instant::from));
       return 1;
-    } catch (ParseException e) {
+    } catch (DateTimeParseException e) {
       throw new CmdLineException(
           owner,
           String.format("Invalid timestamp: %s; expected format: %s", timestamp, TIMESTAMP_FORMAT),
diff --git a/java/com/google/gerrit/server/audit/AuditService.java b/java/com/google/gerrit/server/audit/AuditService.java
index 2a5d868..695c32a 100644
--- a/java/com/google/gerrit/server/audit/AuditService.java
+++ b/java/com/google/gerrit/server/audit/AuditService.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.plugincontext.PluginSetContext;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 @Singleton
 public class AuditService implements GroupAuditService {
@@ -50,7 +50,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<Account.Id> addedMembers,
-      Timestamp addedOn) {
+      Instant addedOn) {
     GroupMemberAuditEvent event =
         GroupMemberAuditEvent.create(actor, updatedGroup, addedMembers, addedOn);
     groupAuditListeners.runEach(l -> l.onAddMembers(event));
@@ -61,7 +61,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<Account.Id> deletedMembers,
-      Timestamp deletedOn) {
+      Instant deletedOn) {
     GroupMemberAuditEvent event =
         GroupMemberAuditEvent.create(actor, updatedGroup, deletedMembers, deletedOn);
     groupAuditListeners.runEach(l -> l.onDeleteMembers(event));
@@ -72,7 +72,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<AccountGroup.UUID> addedSubgroups,
-      Timestamp addedOn) {
+      Instant addedOn) {
     GroupSubgroupAuditEvent event =
         GroupSubgroupAuditEvent.create(actor, updatedGroup, addedSubgroups, addedOn);
     groupAuditListeners.runEach(l -> l.onAddSubgroups(event));
@@ -83,7 +83,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<AccountGroup.UUID> deletedSubgroups,
-      Timestamp deletedOn) {
+      Instant deletedOn) {
     GroupSubgroupAuditEvent event =
         GroupSubgroupAuditEvent.create(actor, updatedGroup, deletedSubgroups, deletedOn);
     groupAuditListeners.runEach(l -> l.onDeleteSubgroups(event));
diff --git a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
index 252a1e2..c37c583 100644
--- a/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupAuditEvent.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** An audit event for groups. */
 public interface GroupAuditEvent {
@@ -35,9 +35,9 @@
   AccountGroup.UUID getUpdatedGroup();
 
   /**
-   * Gets the {@link Timestamp} of the action.
+   * Gets the {@link Instant} of the action.
    *
-   * @return the {@link Timestamp} of the action.
+   * @return the {@link Instant} of the action.
    */
-  Timestamp getTimestamp();
+  Instant getTimestamp();
 }
diff --git a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
index eccfbf4..95a2dce 100644
--- a/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupMemberAuditEvent.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 @AutoValue
 public abstract class GroupMemberAuditEvent implements GroupAuditEvent {
@@ -26,7 +26,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<Account.Id> modifiedMembers,
-      Timestamp timestamp) {
+      Instant timestamp) {
     return new AutoValue_GroupMemberAuditEvent(actor, updatedGroup, modifiedMembers, timestamp);
   }
 
@@ -40,5 +40,5 @@
   public abstract ImmutableSet<Account.Id> getModifiedMembers();
 
   @Override
-  public abstract Timestamp getTimestamp();
+  public abstract Instant getTimestamp();
 }
diff --git a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
index 0fe3962..ace8312 100644
--- a/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
+++ b/java/com/google/gerrit/server/audit/group/GroupSubgroupAuditEvent.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 @AutoValue
 public abstract class GroupSubgroupAuditEvent implements GroupAuditEvent {
@@ -26,7 +26,7 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<AccountGroup.UUID> modifiedSubgroups,
-      Timestamp timestamp) {
+      Instant timestamp) {
     return new AutoValue_GroupSubgroupAuditEvent(actor, updatedGroup, modifiedSubgroups, timestamp);
   }
 
@@ -40,5 +40,5 @@
   public abstract ImmutableSet<AccountGroup.UUID> getModifiedSubgroups();
 
   @Override
-  public abstract Timestamp getTimestamp();
+  public abstract Instant getTimestamp();
 }
diff --git a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
index 13b8b12..0403408 100644
--- a/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
+++ b/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -548,7 +548,7 @@
         c.touch = c.conn.prepareStatement("UPDATE data SET accessed=? WHERE k=? AND version=?");
       }
       try {
-        c.touch.setTimestamp(1, TimeUtil.nowTs());
+        c.touch.setTimestamp(1, new Timestamp(TimeUtil.nowMs()));
         keyType.set(c.touch, 2, key);
         c.touch.setInt(3, version);
         c.touch.executeUpdate();
@@ -581,7 +581,7 @@
           c.put.setBytes(2, valueSerializer.serialize(holder.value));
           c.put.setInt(3, version);
           c.put.setTimestamp(4, Timestamp.from(holder.created));
-          c.put.setTimestamp(5, TimeUtil.nowTs());
+          c.put.setTimestamp(5, new Timestamp(TimeUtil.nowMs()));
           c.put.executeUpdate();
           holder.clean = true;
         } finally {
diff --git a/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java b/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
index 7449917..09f3543 100644
--- a/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
+++ b/java/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializer.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.server.cache.proto.Cache;
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper to (de)serialize values for caches. */
 public class InternalGroupSerializer {
@@ -33,7 +33,7 @@
             .setOwnerGroupUUID(AccountGroup.uuid(proto.getOwnerGroupUuid()))
             .setVisibleToAll(proto.getIsVisibleToAll())
             .setGroupUUID(AccountGroup.uuid(proto.getGroupUuid()))
-            .setCreatedOn(new Timestamp(proto.getCreatedOn()))
+            .setCreatedOn(Instant.ofEpochMilli(proto.getCreatedOn()))
             .setMembers(
                 proto.getMembersIdsList().stream()
                     .map(a -> Account.id(a))
@@ -62,7 +62,7 @@
             .setOwnerGroupUuid(autoValue.getOwnerGroupUUID().get())
             .setIsVisibleToAll(autoValue.isVisibleToAll())
             .setGroupUuid(autoValue.getGroupUUID().get())
-            .setCreatedOn(autoValue.getCreatedOn().getTime());
+            .setCreatedOn(autoValue.getCreatedOn().toEpochMilli());
 
     autoValue.getMembers().stream().forEach(m -> builder.addMembersIds(m.get()));
     autoValue.getSubgroups().stream().forEach(s -> builder.addSubgroupUuids(s.get()));
diff --git a/java/com/google/gerrit/server/change/BatchAbandon.java b/java/com/google/gerrit/server/change/BatchAbandon.java
index 65b6a2a..ad6d7fb 100644
--- a/java/com/google/gerrit/server/change/BatchAbandon.java
+++ b/java/com/google/gerrit/server/change/BatchAbandon.java
@@ -68,7 +68,7 @@
       return;
     }
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
-    try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(project, user, TimeUtil.now())) {
       u.setNotify(notify);
       for (ChangeData change : changes) {
         if (!project.equals(change.project())) {
diff --git a/java/com/google/gerrit/server/change/ChangeJson.java b/java/com/google/gerrit/server/change/ChangeJson.java
index 41723c8..674fc65 100644
--- a/java/com/google/gerrit/server/change/ChangeJson.java
+++ b/java/com/google/gerrit/server/change/ChangeJson.java
@@ -553,8 +553,8 @@
       info.subject = c.getSubject();
       info.status = c.getStatus().asChangeStatus();
       info.owner = new AccountInfo(c.getOwner().get());
-      info.created = c.getCreatedOn();
-      info.updated = c.getLastUpdatedOn();
+      info.setCreated(c.getCreatedOn());
+      info.setUpdated(c.getLastUpdatedOn());
       info._number = c.getId().get();
       info.problems = result.problems();
       info.isPrivate = c.isPrivate() ? true : null;
@@ -642,8 +642,8 @@
     out.subject = in.getSubject();
     out.status = in.getStatus().asChangeStatus();
     out.owner = accountLoader.get(in.getOwner());
-    out.created = in.getCreatedOn();
-    out.updated = in.getLastUpdatedOn();
+    out.setCreated(in.getCreatedOn());
+    out.setUpdated(in.getLastUpdatedOn());
     out._number = in.getId().get();
     out.totalCommentCount = cd.totalCommentCount();
     out.unresolvedCommentCount = cd.unresolvedCommentCount();
@@ -775,11 +775,12 @@
     List<ReviewerStatusUpdate> reviewerUpdates = cd.reviewerUpdates();
     List<ReviewerUpdateInfo> result = new ArrayList<>(reviewerUpdates.size());
     for (ReviewerStatusUpdate c : reviewerUpdates) {
-      ReviewerUpdateInfo change = new ReviewerUpdateInfo();
-      change.updated = c.date();
-      change.state = c.state().asReviewerState();
-      change.updatedBy = accountLoader.get(c.updatedBy());
-      change.reviewer = accountLoader.get(c.reviewer());
+      ReviewerUpdateInfo change =
+          new ReviewerUpdateInfo(
+              c.date(),
+              accountLoader.get(c.updatedBy()),
+              accountLoader.get(c.reviewer()),
+              c.state().asReviewerState());
       result.add(change);
     }
     return result;
@@ -801,8 +802,7 @@
     if (!s.isPresent()) {
       return;
     }
-    out.submitted = s.get().granted();
-    out.submitter = accountLoader.get(s.get().accountId());
+    out.setSubmitted(s.get().granted(), accountLoader.get(s.get().accountId()));
   }
 
   private ImmutableList<ChangeMessageInfo> messages(ChangeData cd) {
diff --git a/java/com/google/gerrit/server/change/ChangeResource.java b/java/com/google/gerrit/server/change/ChangeResource.java
index 6951e7f..919586e 100644
--- a/java/com/google/gerrit/server/change/ChangeResource.java
+++ b/java/com/google/gerrit/server/change/ChangeResource.java
@@ -165,7 +165,7 @@
   // unrelated to the UI.
   public void prepareETag(Hasher h, CurrentUser user) {
     h.putInt(JSON_FORMAT_VERSION)
-        .putLong(getChange().getLastUpdatedOn().getTime())
+        .putLong(getChange().getLastUpdatedOn().toEpochMilli())
         .putInt(user.isIdentifiedUser() ? user.getAccountId().get() : 0);
 
     if (user.isIdentifiedUser()) {
diff --git a/java/com/google/gerrit/server/change/ConsistencyChecker.java b/java/com/google/gerrit/server/change/ConsistencyChecker.java
index 2b0d512..0775647 100644
--- a/java/com/google/gerrit/server/change/ConsistencyChecker.java
+++ b/java/com/google/gerrit/server/change/ConsistencyChecker.java
@@ -626,7 +626,7 @@
   }
 
   private BatchUpdate newBatchUpdate() {
-    return updateFactory.create(change().getProject(), user.get(), TimeUtil.nowTs());
+    return updateFactory.create(change().getProject(), user.get(), TimeUtil.now());
   }
 
   private void fixPatchSetRef(ProblemInfo p, PatchSet ps) {
diff --git a/java/com/google/gerrit/server/change/DeleteReviewerOp.java b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
index 415383a..0116b01 100644
--- a/java/com/google/gerrit/server/change/DeleteReviewerOp.java
+++ b/java/com/google/gerrit/server/change/DeleteReviewerOp.java
@@ -195,7 +195,12 @@
       try {
         if (notify.shouldNotify()) {
           emailReviewers(
-              ctx.getProject(), currChange, mailMessage, ctx.getWhen(), notify, ctx.getRepoView());
+              ctx.getProject(),
+              currChange,
+              mailMessage,
+              Timestamp.from(ctx.getWhen()),
+              notify,
+              ctx.getRepoView());
         }
       } catch (Exception err) {
         logger.atSevere().withCause(err).log(
@@ -251,7 +256,7 @@
         deleteReviewerSenderFactory.create(projectName, change.getId());
     emailSender.setFrom(userId);
     emailSender.addReviewers(Collections.singleton(reviewer.id()));
-    emailSender.setChangeMessage(mailMessage, timestamp);
+    emailSender.setChangeMessage(mailMessage, timestamp.toInstant());
     emailSender.setNotify(notify);
     emailSender.setMessageId(
         messageIdGenerator.fromChangeUpdate(repoView, change.currentPatchSetId()));
diff --git a/java/com/google/gerrit/server/change/EmailReviewComments.java b/java/com/google/gerrit/server/change/EmailReviewComments.java
index 3c7ea44..f94e592 100644
--- a/java/com/google/gerrit/server/change/EmailReviewComments.java
+++ b/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -33,7 +33,7 @@
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
@@ -66,7 +66,7 @@
         PatchSet patchSet,
         IdentifiedUser user,
         @Assisted("message") String message,
-        Timestamp timestamp,
+        Instant timestamp,
         List<? extends Comment> comments,
         @Assisted("patchSetComment") String patchSetComment,
         List<LabelVote> labels,
@@ -84,7 +84,7 @@
   private final PatchSet patchSet;
   private final IdentifiedUser user;
   private final String message;
-  private final Timestamp timestamp;
+  private final Instant timestamp;
   private final List<? extends Comment> comments;
   private final String patchSetComment;
   private final List<LabelVote> labels;
@@ -102,7 +102,7 @@
       @Assisted PatchSet patchSet,
       @Assisted IdentifiedUser user,
       @Assisted("message") String message,
-      @Assisted Timestamp timestamp,
+      @Assisted Instant timestamp,
       @Assisted List<? extends Comment> comments,
       @Nullable @Assisted("patchSetComment") String patchSetComment,
       @Assisted List<LabelVote> labels,
diff --git a/java/com/google/gerrit/server/change/LabelsJson.java b/java/com/google/gerrit/server/change/LabelsJson.java
index 0bc0f64..325b20a 100644
--- a/java/com/google/gerrit/server/change/LabelsJson.java
+++ b/java/com/google/gerrit/server/change/LabelsJson.java
@@ -48,7 +48,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -199,10 +199,10 @@
   private ApprovalInfo approvalInfo(
       AccountLoader accountLoader,
       Account.Id id,
-      Integer value,
-      VotingRangeInfo permittedVotingRange,
-      String tag,
-      Timestamp date) {
+      @Nullable Integer value,
+      @Nullable VotingRangeInfo permittedVotingRange,
+      @Nullable String tag,
+      @Nullable Instant date) {
     ApprovalInfo ai = new ApprovalInfo(id.get(), value, permittedVotingRange, tag, date);
     accountLoader.put(ai);
     return ai;
@@ -309,7 +309,7 @@
         if (info != null) {
           info.value = Integer.valueOf(val);
           info.permittedVotingRange = pvr.getOrDefault(type.get().getName(), null);
-          info.date = psa.granted();
+          info.setDate(psa.granted());
           info.tag = psa.tag().orElse(null);
           if (psa.postSubmit()) {
             info.postSubmit = true;
@@ -442,7 +442,7 @@
         Integer value;
         VotingRangeInfo permittedVotingRange = pvr.getOrDefault(lt.get().getName(), null);
         String tag = null;
-        Timestamp date = null;
+        Instant date = null;
         PatchSetApproval psa = current.get(accountId, lt.get().getName());
         if (psa != null) {
           value = Integer.valueOf(psa.value());
diff --git a/java/com/google/gerrit/server/change/RebaseChangeOp.java b/java/com/google/gerrit/server/change/RebaseChangeOp.java
index 3e67cca..57f94ff 100644
--- a/java/com/google/gerrit/server/change/RebaseChangeOp.java
+++ b/java/com/google/gerrit/server/change/RebaseChangeOp.java
@@ -392,7 +392,7 @@
     if (committerIdent != null) {
       cb.setCommitter(committerIdent);
     } else {
-      cb.setCommitter(ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone()));
+      cb.setCommitter(ctx.newCommitterIdent());
     }
     if (matchAuthorToCommitterDate) {
       cb.setAuthor(
diff --git a/java/com/google/gerrit/server/change/RevisionJson.java b/java/com/google/gerrit/server/change/RevisionJson.java
index 33f3d4f..5a2a0eb 100644
--- a/java/com/google/gerrit/server/change/RevisionJson.java
+++ b/java/com/google/gerrit/server/change/RevisionJson.java
@@ -285,7 +285,7 @@
     out.isCurrent = in.id().equals(c.currentPatchSetId());
     out._number = in.id().get();
     out.ref = in.refName();
-    out.created = in.createdOn();
+    out.setCreated(in.createdOn());
     out.uploader = accountLoader.get(in.uploader());
     out.fetch = makeFetchMap(cd, in);
     out.kind = changeKindCache.getChangeKind(rw, repo != null ? repo.getConfig() : null, cd, in);
diff --git a/java/com/google/gerrit/server/edit/ChangeEditModifier.java b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
index bc905c2..81f98e6 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditModifier.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditModifier.java
@@ -52,7 +52,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -139,7 +139,7 @@
 
     PatchSet currentPatchSet = lookupCurrentPatchSet(notes);
     ObjectId patchSetCommitId = currentPatchSet.commitId();
-    noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.nowTs());
+    noteDbEdits.createEdit(repository, notes, currentPatchSet, patchSetCommitId, TimeUtil.now());
   }
 
   /**
@@ -187,7 +187,7 @@
     RevTree basePatchSetTree = basePatchSetCommit.getTree();
 
     ObjectId newTreeId = merge(repository, changeEdit, basePatchSetTree);
-    Timestamp nowTimestamp = TimeUtil.nowTs();
+    Instant nowTimestamp = TimeUtil.now();
     String commitMessage = currentEditCommit.getFullMessage();
     ObjectId newEditCommitId =
         createCommit(repository, basePatchSetCommit, newTreeId, commitMessage, nowTimestamp);
@@ -385,7 +385,7 @@
       return unmodifiedEdit.get();
     }
 
-    Timestamp nowTimestamp = TimeUtil.nowTs();
+    Instant nowTimestamp = TimeUtil.now();
     ObjectId newEditCommit =
         createCommit(repository, basePatchsetCommit, newTreeId, newCommitMessage, nowTimestamp);
 
@@ -501,7 +501,7 @@
       RevCommit basePatchsetCommit,
       ObjectId tree,
       String commitMessage,
-      Timestamp timestamp)
+      Instant timestamp)
       throws IOException {
     try (ObjectInserter objectInserter = repository.newObjectInserter()) {
       CommitBuilder builder = new CommitBuilder();
@@ -516,7 +516,7 @@
     }
   }
 
-  private PersonIdent getCommitterIdent(Timestamp commitTimestamp) {
+  private PersonIdent getCommitterIdent(Instant commitTimestamp) {
     IdentifiedUser user = currentUser.get().asIdentifiedUser();
     return user.newCommitterIdent(commitTimestamp, tz);
   }
@@ -547,7 +547,7 @@
         ChangeNotes notes,
         PatchSet basePatchSet,
         ObjectId newEditCommitId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException;
   }
 
@@ -647,7 +647,7 @@
         ChangeNotes notes,
         PatchSet basePatchSet,
         ObjectId newEditCommitId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       return noteDbEdits.updateEdit(
           notes.getProjectName(), repository, changeEdit, newEditCommitId, timestamp);
@@ -701,7 +701,7 @@
         ChangeNotes notes,
         PatchSet basePatchSet,
         ObjectId newEditCommitId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       return noteDbEdits.createEdit(repository, notes, basePatchSet, newEditCommitId, timestamp);
     }
@@ -723,7 +723,7 @@
         ChangeNotes notes,
         PatchSet basePatchset,
         ObjectId newEditCommitId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       Change change = notes.getChange();
       String editRefName = getEditRefName(change, basePatchset);
@@ -750,7 +750,7 @@
         Repository repository,
         ChangeEdit changeEdit,
         ObjectId newEditCommitId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       String editRefName = changeEdit.getRefName();
       RevCommit currentEditCommit = changeEdit.getEditCommit();
@@ -769,7 +769,7 @@
         String refName,
         ObjectId currentObjectId,
         ObjectId targetObjectId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       RefUpdate ru = repository.updateRef(refName);
       ru.setExpectedOldObjectId(currentObjectId);
@@ -795,7 +795,7 @@
         PatchSet currentPatchSet,
         ObjectId currentEditCommit,
         ObjectId newEditCommitId,
-        Timestamp nowTimestamp)
+        Instant nowTimestamp)
         throws IOException {
       String newEditRefName = getEditRefName(changeEdit.getChange(), currentPatchSet);
       updateReferenceWithNameChange(
@@ -814,7 +814,7 @@
         ObjectId currentObjectId,
         String newRefName,
         ObjectId targetObjectId,
-        Timestamp timestamp)
+        Instant timestamp)
         throws IOException {
       BatchRefUpdate batchRefUpdate = repository.getRefDatabase().newBatchUpdate();
       batchRefUpdate.addCommand(new ReceiveCommand(ObjectId.zeroId(), targetObjectId, newRefName));
@@ -838,7 +838,7 @@
       }
     }
 
-    private PersonIdent getRefLogIdent(Timestamp timestamp) {
+    private PersonIdent getRefLogIdent(Instant timestamp) {
       IdentifiedUser user = currentUser.get().asIdentifiedUser();
       return user.newRefLogIdent(timestamp, tz);
     }
diff --git a/java/com/google/gerrit/server/edit/ChangeEditUtil.java b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
index 6b018ce..74834ab 100644
--- a/java/com/google/gerrit/server/edit/ChangeEditUtil.java
+++ b/java/com/google/gerrit/server/edit/ChangeEditUtil.java
@@ -185,7 +185,7 @@
         message.append("Published edit on patch set ").append(basePatchSet.number()).append(".");
       }
 
-      try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.nowTs())) {
+      try (BatchUpdate bu = updateFactory.create(change.getProject(), user, TimeUtil.now())) {
         bu.setRepository(repo, rw, oi);
         bu.setNotify(notify);
         bu.addOp(change.getId(), inserter.setMessage(message.toString()));
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 2a2bf73..2b402a6 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -139,7 +139,7 @@
     a.owner = asAccountAttribute(change.getOwner());
     a.assignee = asAccountAttribute(change.getAssignee());
     a.status = change.getStatus();
-    a.createdOn = change.getCreatedOn().getTime() / 1000L;
+    a.createdOn = change.getCreatedOn().getEpochSecond();
     a.wip = change.isWorkInProgress() ? true : null;
     a.isPrivate = change.isPrivate() ? true : null;
     a.cherryPickOfChange =
@@ -175,7 +175,7 @@
 
   /** Extend the existing {@link ChangeAttribute} with additional fields. */
   public void extend(ChangeAttribute a, Change change) {
-    a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
+    a.lastUpdated = change.getLastUpdatedOn().getEpochSecond();
     a.open = change.isNew();
   }
 
@@ -437,7 +437,7 @@
     p.number = patchSet.number();
     p.ref = patchSet.refName();
     p.uploader = asAccountAttribute(patchSet.uploader());
-    p.createdOn = patchSet.createdOn().getTime() / 1000L;
+    p.createdOn = patchSet.createdOn().getEpochSecond();
     PatchSet.Id pId = patchSet.id();
     try {
       p.parents = new ArrayList<>();
@@ -534,7 +534,7 @@
     a.type = approval.labelId().get();
     a.value = Short.toString(approval.value());
     a.by = asAccountAttribute(approval.accountId());
-    a.grantedOn = approval.granted().getTime() / 1000L;
+    a.grantedOn = approval.granted().getEpochSecond();
     a.oldValue = null;
 
     Optional<LabelType> lt = labelTypes.byLabel(approval.labelId());
@@ -544,7 +544,7 @@
 
   public MessageAttribute asMessageAttribute(ChangeMessage message) {
     MessageAttribute a = new MessageAttribute();
-    a.timestamp = message.getWrittenOn().getTime() / 1000L;
+    a.timestamp = message.getWrittenOn().getEpochSecond();
     a.reviewer =
         message.getAuthor() != null
             ? asAccountAttribute(message.getAuthor())
diff --git a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
index b7ee043..fde4088 100644
--- a/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/AbstractChangeEvent.java
@@ -18,17 +18,17 @@
 import com.google.gerrit.extensions.common.AccountInfo;
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.events.ChangeEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Base class for all change events. */
 public abstract class AbstractChangeEvent implements ChangeEvent {
   private final ChangeInfo changeInfo;
   private final AccountInfo who;
-  private final Timestamp when;
+  private final Instant when;
   private final NotifyHandling notify;
 
   protected AbstractChangeEvent(
-      ChangeInfo change, AccountInfo who, Timestamp when, NotifyHandling notify) {
+      ChangeInfo change, AccountInfo who, Instant when, NotifyHandling notify) {
     this.changeInfo = change;
     this.who = who;
     this.when = when;
@@ -46,7 +46,7 @@
   }
 
   @Override
-  public Timestamp getWhen() {
+  public Instant getWhen() {
     return when;
   }
 
diff --git a/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java b/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
index 9d4d299..421a5ad 100644
--- a/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
+++ b/java/com/google/gerrit/server/extensions/events/AbstractRevisionEvent.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.extensions.common.ChangeInfo;
 import com.google.gerrit.extensions.common.RevisionInfo;
 import com.google.gerrit.extensions.events.RevisionEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Base class for all revision events. */
 public abstract class AbstractRevisionEvent extends AbstractChangeEvent implements RevisionEvent {
@@ -30,7 +30,7 @@
       ChangeInfo change,
       RevisionInfo revision,
       AccountInfo who,
-      Timestamp when,
+      Instant when,
       NotifyHandling notify) {
     super(change, who, when, notify);
     revisionInfo = revision;
diff --git a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
index e31a1b5..8e4d1e2 100644
--- a/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/AssigneeChanged.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a user has been set as assignee on a change. */
 @Singleton
@@ -42,7 +42,7 @@
   }
 
   public void fire(
-      ChangeData changeData, AccountState accountState, AccountState oldAssignee, Timestamp when) {
+      ChangeData changeData, AccountState accountState, AccountState oldAssignee, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -63,7 +63,7 @@
   private static class Event extends AbstractChangeEvent implements AssigneeChangedListener.Event {
     private final AccountInfo oldAssignee;
 
-    Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, Timestamp when) {
+    Event(ChangeInfo change, AccountInfo editor, AccountInfo oldAssignee, Instant when) {
       super(change, editor, when, NotifyHandling.ALL);
       this.oldAssignee = oldAssignee;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
index cbe7c6b..ca1a742 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeAbandoned.java
@@ -32,7 +32,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a change has been abandoned. */
 @Singleton
@@ -53,7 +53,7 @@
       PatchSet ps,
       AccountState abandoner,
       String reason,
-      Timestamp when,
+      Instant when,
       NotifyHandling notifyHandling) {
     if (listeners.isEmpty()) {
       return;
@@ -89,7 +89,7 @@
         RevisionInfo revision,
         AccountInfo abandoner,
         String reason,
-        Timestamp when,
+        Instant when,
         NotifyHandling notifyHandling) {
       super(change, revision, abandoner, when, notifyHandling);
       this.reason = reason;
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
index 23a4583..acca491 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeDeleted.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a change has been deleted. */
 @Singleton
@@ -41,7 +41,7 @@
     this.util = util;
   }
 
-  public void fire(ChangeData changeData, AccountState deleter, Timestamp when) {
+  public void fire(ChangeData changeData, AccountState deleter, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -55,7 +55,7 @@
 
   /** Event to be fired when a change has been deleted. */
   private static class Event extends AbstractChangeEvent implements ChangeDeletedListener.Event {
-    Event(ChangeInfo change, AccountInfo deleter, Timestamp when) {
+    Event(ChangeInfo change, AccountInfo deleter, Instant when) {
       super(change, deleter, when, NotifyHandling.ALL);
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
index e4896df..870d850 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeMerged.java
@@ -32,7 +32,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a change has been merged. */
 @Singleton
@@ -49,11 +49,7 @@
   }
 
   public void fire(
-      ChangeData changeData,
-      PatchSet ps,
-      AccountState merger,
-      String newRevisionId,
-      Timestamp when) {
+      ChangeData changeData, PatchSet ps, AccountState merger, String newRevisionId, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -86,7 +82,7 @@
         RevisionInfo revision,
         AccountInfo merger,
         String newRevisionId,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, merger, when, NotifyHandling.ALL);
       this.newRevisionId = newRevisionId;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
index 8bd222a..c71360b 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeRestored.java
@@ -32,7 +32,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a change has been restored. */
 @Singleton
@@ -49,7 +49,7 @@
   }
 
   public void fire(
-      ChangeData changeData, PatchSet ps, AccountState restorer, String reason, Timestamp when) {
+      ChangeData changeData, PatchSet ps, AccountState restorer, String reason, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -83,7 +83,7 @@
         RevisionInfo revision,
         AccountInfo restorer,
         String reason,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, restorer, when, NotifyHandling.ALL);
       this.reason = reason;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
index 4a46eb0..1abbebb 100644
--- a/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
+++ b/java/com/google/gerrit/server/extensions/events/ChangeReverted.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a change has been reverted. */
 @Singleton
@@ -39,7 +39,7 @@
     this.util = util;
   }
 
-  public void fire(ChangeData changeData, ChangeData revertChangeData, Timestamp when) {
+  public void fire(ChangeData changeData, ChangeData revertChangeData, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -55,7 +55,7 @@
   private static class Event extends AbstractChangeEvent implements ChangeRevertedListener.Event {
     private final ChangeInfo revertChange;
 
-    Event(ChangeInfo change, ChangeInfo revertChange, Timestamp when) {
+    Event(ChangeInfo change, ChangeInfo revertChange, Instant when) {
       super(change, revertChange.owner, when, NotifyHandling.ALL);
       this.revertChange = revertChange;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/CommentAdded.java b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
index 20c54cf..79544f2 100644
--- a/java/com/google/gerrit/server/extensions/events/CommentAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/CommentAdded.java
@@ -33,7 +33,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Map;
 
 /** Helper class to fire an event when a comment or vote has been added to a change. */
@@ -57,7 +57,7 @@
       String comment,
       Map<String, Short> approvals,
       Map<String, Short> oldApprovals,
-      Timestamp when) {
+      Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -97,7 +97,7 @@
         String comment,
         Map<String, ApprovalInfo> approvals,
         Map<String, ApprovalInfo> oldApprovals,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, author, when, NotifyHandling.ALL);
       this.comment = comment;
       this.approvals = approvals;
diff --git a/java/com/google/gerrit/server/extensions/events/EventUtil.java b/java/com/google/gerrit/server/extensions/events/EventUtil.java
index f0d038a..45f7ecb 100644
--- a/java/com/google/gerrit/server/extensions/events/EventUtil.java
+++ b/java/com/google/gerrit/server/extensions/events/EventUtil.java
@@ -36,7 +36,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.Map;
@@ -111,7 +111,7 @@
   }
 
   public Map<String, ApprovalInfo> approvals(
-      AccountState accountState, Map<String, Short> approvals, Timestamp ts) {
+      AccountState accountState, Map<String, Short> approvals, Instant ts) {
     Map<String, ApprovalInfo> result = new HashMap<>();
     for (Map.Entry<String, Short> e : approvals.entrySet()) {
       Integer value = e.getValue() != null ? Integer.valueOf(e.getValue()) : null;
diff --git a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
index 846257c..e7903a2 100644
--- a/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/HashtagsEdited.java
@@ -26,7 +26,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collection;
 import java.util.Set;
 
@@ -50,7 +50,7 @@
       ImmutableSortedSet<String> hashtags,
       Set<String> added,
       Set<String> removed,
-      Timestamp when) {
+      Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -82,7 +82,7 @@
         Collection<String> updated,
         Collection<String> added,
         Collection<String> removed,
-        Timestamp when) {
+        Instant when) {
       super(change, editor, when, NotifyHandling.ALL);
       this.updatedHashtags = updated;
       this.addedHashtags = added;
diff --git a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
index d81068c..c6076fd 100644
--- a/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/PrivateStateChanged.java
@@ -31,7 +31,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when the private flag of a change has been toggled. */
 @Singleton
@@ -47,7 +47,7 @@
     this.util = util;
   }
 
-  public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {
+  public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -72,7 +72,7 @@
   private static class Event extends AbstractRevisionEvent
       implements PrivateStateChangedListener.Event {
 
-    protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+    protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Instant when) {
       super(change, revision, who, when, NotifyHandling.ALL);
     }
   }
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
index ba73ca1..147e372 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerAdded.java
@@ -33,7 +33,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.List;
 
 /** Helper class to fire an event when reviewers have been added to a change. */
@@ -55,7 +55,7 @@
       PatchSet patchSet,
       List<AccountState> reviewers,
       AccountState adder,
-      Timestamp when) {
+      Instant when) {
     if (listeners.isEmpty() || reviewers.isEmpty()) {
       return;
     }
@@ -89,7 +89,7 @@
         RevisionInfo revision,
         List<AccountInfo> reviewers,
         AccountInfo adder,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, adder, when, NotifyHandling.ALL);
       this.reviewers = reviewers;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
index 80037bc..5f9179a 100644
--- a/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/ReviewerDeleted.java
@@ -33,7 +33,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Map;
 
 /** Helper class to fire an event when a reviewer has been deleted from a change. */
@@ -59,7 +59,7 @@
       Map<String, Short> newApprovals,
       Map<String, Short> oldApprovals,
       NotifyHandling notify,
-      Timestamp when) {
+      Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -104,7 +104,7 @@
         Map<String, ApprovalInfo> newApprovals,
         Map<String, ApprovalInfo> oldApprovals,
         NotifyHandling notify,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, remover, when, notify);
       this.reviewer = reviewer;
       this.comment = comment;
diff --git a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
index 4c78216..a60d982 100644
--- a/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
+++ b/java/com/google/gerrit/server/extensions/events/RevisionCreated.java
@@ -33,7 +33,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when a revision has been created for a change. */
 @Singleton
@@ -47,7 +47,7 @@
             ChangeData changeData,
             PatchSet patchSet,
             AccountState uploader,
-            Timestamp when,
+            Instant when,
             NotifyResolver.Result notify) {}
       };
 
@@ -69,7 +69,7 @@
       ChangeData changeData,
       PatchSet patchSet,
       AccountState uploader,
-      Timestamp when,
+      Instant when,
       NotifyResolver.Result notify) {
     if (listeners.isEmpty()) {
       return;
@@ -102,7 +102,7 @@
         ChangeInfo change,
         RevisionInfo revision,
         AccountInfo uploader,
-        Timestamp when,
+        Instant when,
         NotifyHandling notify) {
       super(change, revision, uploader, when, notify);
     }
diff --git a/java/com/google/gerrit/server/extensions/events/TopicEdited.java b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
index 08b47f1..008ead5 100644
--- a/java/com/google/gerrit/server/extensions/events/TopicEdited.java
+++ b/java/com/google/gerrit/server/extensions/events/TopicEdited.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when the topic of a change has been edited. */
 @Singleton
@@ -41,8 +41,7 @@
     this.util = util;
   }
 
-  public void fire(
-      ChangeData changeData, AccountState account, String oldTopicName, Timestamp when) {
+  public void fire(ChangeData changeData, AccountState account, String oldTopicName, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -59,7 +58,7 @@
   private static class Event extends AbstractChangeEvent implements TopicEditedListener.Event {
     private final String oldTopic;
 
-    Event(ChangeInfo change, AccountInfo editor, String oldTopic, Timestamp when) {
+    Event(ChangeInfo change, AccountInfo editor, String oldTopic, Instant when) {
       super(change, editor, when, NotifyHandling.ALL);
       this.oldTopic = oldTopic;
     }
diff --git a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
index 244e46c..deaaff8 100644
--- a/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
+++ b/java/com/google/gerrit/server/extensions/events/VoteDeleted.java
@@ -33,7 +33,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Map;
 
 /** Helper class to fire an event when a vote has been deleted from a change. */
@@ -59,7 +59,7 @@
       NotifyHandling notify,
       String message,
       AccountState remover,
-      Timestamp when) {
+      Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -103,7 +103,7 @@
         NotifyHandling notify,
         String message,
         AccountInfo remover,
-        Timestamp when) {
+        Instant when) {
       super(change, revision, remover, when, notify);
       this.reviewer = reviewer;
       this.approvals = approvals;
diff --git a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
index bfc068d..5e20c45 100644
--- a/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
+++ b/java/com/google/gerrit/server/extensions/events/WorkInProgressStateChanged.java
@@ -31,7 +31,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 /** Helper class to fire an event when the work-in-progress state of a change has been toggled. */
 @Singleton
@@ -42,7 +42,7 @@
       new WorkInProgressStateChanged() {
         @Override
         public void fire(
-            ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {}
+            ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {}
       };
 
   private final PluginSetContext<WorkInProgressStateChangedListener> listeners;
@@ -60,7 +60,7 @@
     this.util = null;
   }
 
-  public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Timestamp when) {
+  public void fire(ChangeData changeData, PatchSet patchSet, AccountState account, Instant when) {
     if (listeners.isEmpty()) {
       return;
     }
@@ -85,7 +85,7 @@
   private static class Event extends AbstractRevisionEvent
       implements WorkInProgressStateChangedListener.Event {
 
-    protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Timestamp when) {
+    protected Event(ChangeInfo change, RevisionInfo revision, AccountInfo who, Instant when) {
       super(change, revision, who, when, NotifyHandling.ALL);
     }
   }
diff --git a/java/com/google/gerrit/server/git/BanCommit.java b/java/com/google/gerrit/server/git/BanCommit.java
index 242c11b..9cc754c 100644
--- a/java/com/google/gerrit/server/git/BanCommit.java
+++ b/java/com/google/gerrit/server/git/BanCommit.java
@@ -30,7 +30,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.util.Date;
+import java.time.Instant;
 import java.util.List;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -155,8 +155,7 @@
   }
 
   private PersonIdent createPersonIdent() {
-    Date now = new Date();
-    return currentUser.get().newCommitterIdent(now, tz);
+    return currentUser.get().newCommitterIdent(Instant.now(), tz);
   }
 
   private static String buildCommitMessage(List<ObjectId> bannedCommits, String reason) {
diff --git a/java/com/google/gerrit/server/git/CommitUtil.java b/java/com/google/gerrit/server/git/CommitUtil.java
index 73378f6..e52c45f 100644
--- a/java/com/google/gerrit/server/git/CommitUtil.java
+++ b/java/com/google/gerrit/server/git/CommitUtil.java
@@ -53,8 +53,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.Set;
@@ -146,7 +146,7 @@
    * @return ObjectId that represents the newly created commit.
    */
   public Change.Id createRevertChange(
-      ChangeNotes notes, CurrentUser user, RevertInput input, Timestamp timestamp)
+      ChangeNotes notes, CurrentUser user, RevertInput input, Instant timestamp)
       throws RestApiException, UpdateException, ConfigInvalidException, IOException {
     String message = Strings.emptyToNull(input.message);
 
@@ -174,7 +174,7 @@
    * @return ObjectId that represents the newly created commit.
    */
   public ObjectId createRevertCommit(
-      String message, ChangeNotes notes, CurrentUser user, Timestamp ts)
+      String message, ChangeNotes notes, CurrentUser user, Instant ts)
       throws RestApiException, IOException {
 
     try (Repository git = repoManager.openRepository(notes.getProjectName());
@@ -206,7 +206,7 @@
       String message,
       ChangeNotes notes,
       CurrentUser user,
-      Timestamp ts,
+      Instant ts,
       ObjectInserter oi,
       RevWalk revWalk,
       @Nullable ObjectId generatedChangeId)
@@ -255,7 +255,7 @@
       ChangeNotes notes,
       CurrentUser user,
       @Nullable ObjectId generatedChangeId,
-      Timestamp ts,
+      Instant ts,
       ObjectInserter oi,
       RevWalk revWalk,
       Repository git)
diff --git a/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 09f08bd..a49260f 100644
--- a/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -36,6 +36,8 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ProgressMonitor;
 
@@ -154,6 +156,64 @@
         return count;
       }
     }
+
+    public int getTotal() {
+      return total;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public String getTotalDisplay(int total) {
+      return String.valueOf(total);
+    }
+  }
+
+  /** Handle for a sub-task whose total work can be updated while the task is in progress. */
+  public class VolatileTask extends Task {
+    protected AtomicInteger volatileTotal;
+    protected AtomicBoolean isTotalFinalized = new AtomicBoolean(false);
+
+    public VolatileTask(String subTaskName) {
+      super(subTaskName, UNKNOWN);
+      volatileTotal = new AtomicInteger(UNKNOWN);
+    }
+
+    /**
+     * Update the total work for this sub-task.
+     *
+     * <p>Intended to be called from a worker thread.
+     *
+     * @param workUnits number of work units to be added to existing total work.
+     */
+    public void updateTotal(int workUnits) {
+      if (!isTotalFinalized.get()) {
+        volatileTotal.addAndGet(workUnits);
+      } else {
+        logger.atWarning().log(
+            "Total work has been finalized on sub-task " + getName() + " and cannot be updated");
+      }
+    }
+
+    /**
+     * Mark the total on this sub-task as unmodifiable.
+     *
+     * <p>Intended to be called from a worker thread.
+     */
+    public void finalizeTotal() {
+      isTotalFinalized.set(true);
+    }
+
+    @Override
+    public int getTotal() {
+      return volatileTotal.get();
+    }
+
+    @Override
+    public String getTotalDisplay(int total) {
+      return super.getTotalDisplay(total) + (isTotalFinalized.get() ? "" : "+");
+    }
   }
 
   public interface Factory {
@@ -249,6 +309,7 @@
    * calls {@link #end()}, the future has an additional {@code maxInterval} to finish before it is
    * forcefully cancelled and {@link ExecutionException} is thrown.
    *
+   * @see #waitForNonFinalTask(Future, long, TimeUnit)
    * @param workerFuture a future that returns when worker threads are finished.
    * @param taskTimeoutTime overall timeout for the task; the future gets a cancellation signal
    *     after this timeout is exceeded; non-positive values indicate no timeout.
@@ -267,6 +328,60 @@
       long cancellationTimeoutTime,
       TimeUnit cancellationTimeoutUnit)
       throws TimeoutException {
+    T t =
+        waitForNonFinalTask(
+            workerFuture,
+            taskTimeoutTime,
+            taskTimeoutUnit,
+            cancellationTimeoutTime,
+            cancellationTimeoutUnit);
+    synchronized (this) {
+      if (!done) {
+        // The worker may not have called end() explicitly, which is likely a
+        // programming error.
+        logger.atWarning().log("MultiProgressMonitor worker did not call end() before returning");
+        end();
+      }
+    }
+    sendDone();
+    return t;
+  }
+
+  /**
+   * Wait for a non-final task managed by a {@link Future}, with no timeout.
+   *
+   * @see #waitForNonFinalTask(Future, long, TimeUnit)
+   */
+  public <T> T waitForNonFinalTask(Future<T> workerFuture) {
+    try {
+      return waitForNonFinalTask(workerFuture, 0, null, 0, null);
+    } catch (TimeoutException e) {
+      throw new IllegalStateException("timout exception without setting a timeout", e);
+    }
+  }
+
+  /**
+   * Wait for a task managed by a {@link Future}. This call does not expect the worker thread to
+   * call {@link #end()}. It is intended to be used to track a non-final task.
+   *
+   * @param workerFuture a future that returns when worker threads are finished.
+   * @param taskTimeoutTime overall timeout for the task; the future is forcefully cancelled if the
+   *     task exceeds the timeout. Non-positive values indicate no timeout.
+   * @param taskTimeoutUnit unit for overall task timeout.
+   * @param cancellationTimeoutTime timeout for the task to react to the cancellation signal; if the
+   *     task doesn't terminate within this time it is forcefully cancelled; non-positive values
+   *     indicate no timeout.
+   * @param cancellationTimeoutUnit unit for the cancellation timeout.
+   * @throws TimeoutException if this thread or a worker thread was interrupted, the worker was
+   *     cancelled, or timed out waiting for a worker to call {@link #end()}.
+   */
+  public <T> T waitForNonFinalTask(
+      Future<T> workerFuture,
+      long taskTimeoutTime,
+      TimeUnit taskTimeoutUnit,
+      long cancellationTimeoutTime,
+      TimeUnit cancellationTimeoutUnit)
+      throws TimeoutException {
     long overallStart = ticker.read();
     long cancellationNanos =
         cancellationTimeoutTime > 0
@@ -282,7 +397,7 @@
 
     synchronized (this) {
       long left = maxIntervalNanos;
-      while (!done) {
+      while (!workerFuture.isDone() && !done) {
         long start = ticker.read();
         try {
           // Conditions below gives better granularity for timeouts.
@@ -349,19 +464,11 @@
           left = maxIntervalNanos;
         }
         sendUpdate();
-        if (!done && workerFuture.isDone()) {
-          // The worker may not have called end() explicitly, which is likely a
-          // programming error.
-          logger.atWarning().log(
-              "MultiProgressMonitor worker did not call end() before returning (task=%s(%s))",
-              taskKind, taskName);
-          end();
-        }
       }
       if (deadlineExceeded && !forcefulTermination && taskKind == TaskKind.RECEIVE_COMMITS) {
         cancellationMetrics.countGracefulReceiveTimeout();
       }
-      sendDone();
+      wakeUp();
     }
 
     // The loop exits as soon as the worker calls end(), but we give it another
@@ -398,6 +505,18 @@
   }
 
   /**
+   * Begin a sub-task whose total work can be updated.
+   *
+   * @param subTask sub-task name.
+   * @return sub-task handle.
+   */
+  public VolatileTask beginVolatileSubTask(String subTask) {
+    VolatileTask task = new VolatileTask(subTask);
+    tasks.add(task);
+    return task;
+  }
+
+  /**
    * End the overall task.
    *
    * <p>Must be called from a worker thread.
@@ -440,6 +559,7 @@
       boolean first = true;
       for (Task t : tasks) {
         int count = t.getCount();
+        int total = t.getTotal();
         if (count == 0) {
           continue;
         }
@@ -454,10 +574,11 @@
         if (!Strings.isNullOrEmpty(t.name)) {
           s.append(t.name).append(": ");
         }
-        if (t.total == UNKNOWN) {
+        if (total == UNKNOWN) {
           s.append(count);
         } else {
-          s.append(String.format("%d%% (%d/%d)", count * 100 / t.total, count, t.total));
+          s.append(
+              String.format("%d%% (%d/%s)", count * 100 / total, count, t.getTotalDisplay(total)));
         }
       }
     }
diff --git a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
index 27d5da9..befdb58 100644
--- a/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
+++ b/java/com/google/gerrit/server/git/meta/MetaDataUpdate.java
@@ -135,7 +135,7 @@
 
     private PersonIdent createPersonIdent(IdentifiedUser user) {
       PersonIdent serverIdent = serverIdentProvider.get();
-      return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
+      return user.newCommitterIdent(serverIdent);
     }
   }
 
@@ -215,11 +215,7 @@
 
   public void setAuthor(IdentifiedUser author) {
     this.author = author;
-    getCommitBuilder()
-        .setAuthor(
-            author.newCommitterIdent(
-                getCommitBuilder().getCommitter().getWhen(),
-                getCommitBuilder().getCommitter().getTimeZone()));
+    getCommitBuilder().setAuthor(author.newCommitterIdent(getCommitBuilder().getCommitter()));
   }
 
   public void setAllowEmpty(boolean allowEmpty) {
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 89c025a..41f68c8 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -840,7 +840,7 @@
       Map<BranchNameKey, ReceiveCommand> branches;
       try (BatchUpdate bu =
               batchUpdateFactory.create(
-                  project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+                  project.getNameKey(), user.materializedCopy(), TimeUtil.now());
           ObjectInserter ins = repo.newObjectInserter();
           ObjectReader reader = ins.newReader();
           RevWalk rw = new RevWalk(reader);
@@ -862,7 +862,7 @@
 
         submissionExecutor.execute(ImmutableList.of(bu));
 
-        orm.setContext(TimeUtil.nowTs(), user, NotifyResolver.Result.none());
+        orm.setContext(TimeUtil.now(), user, NotifyResolver.Result.none());
         submissionExecutor.afterExecutions(orm);
 
         branches = bu.getSuccessfullyUpdatedBranches(false);
@@ -1026,7 +1026,7 @@
 
       try (BatchUpdate bu =
               batchUpdateFactory.create(
-                  project.getNameKey(), user.materializedCopy(), TimeUtil.nowTs());
+                  project.getNameKey(), user.materializedCopy(), TimeUtil.now());
           ObjectInserter ins = repo.newObjectInserter();
           ObjectReader reader = ins.newReader();
           RevWalk rw = new RevWalk(reader)) {
@@ -3456,7 +3456,7 @@
                 "autoCloseChanges",
                 updateFactory -> {
                   try (BatchUpdate bu =
-                          updateFactory.create(projectState.getNameKey(), user, TimeUtil.nowTs());
+                          updateFactory.create(projectState.getNameKey(), user, TimeUtil.now());
                       ObjectInserter ins = repo.newObjectInserter();
                       ObjectReader reader = ins.newReader();
                       RevWalk rw = new RevWalk(reader)) {
diff --git a/java/com/google/gerrit/server/group/GroupAuditService.java b/java/com/google/gerrit/server/group/GroupAuditService.java
index 30e5d3c..21959ec 100644
--- a/java/com/google/gerrit/server/group/GroupAuditService.java
+++ b/java/com/google/gerrit/server/group/GroupAuditService.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.server.AuditEvent;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 public interface GroupAuditService {
   void dispatch(AuditEvent action);
@@ -27,23 +27,23 @@
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<Account.Id> addedMembers,
-      Timestamp addedOn);
+      Instant addedOn);
 
   void dispatchDeleteMembers(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<Account.Id> deletedMembers,
-      Timestamp deletedOn);
+      Instant deletedOn);
 
   void dispatchAddSubgroups(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<AccountGroup.UUID> addedSubgroups,
-      Timestamp addedOn);
+      Instant addedOn);
 
   void dispatchDeleteSubgroups(
       Account.Id actor,
       AccountGroup.UUID updatedGroup,
       ImmutableSet<AccountGroup.UUID> deletedSubgroups,
-      Timestamp deletedOn);
+      Instant deletedOn);
 }
diff --git a/java/com/google/gerrit/server/group/InternalGroupDescription.java b/java/com/google/gerrit/server/group/InternalGroupDescription.java
index 62ebcfe..984daea 100644
--- a/java/com/google/gerrit/server/group/InternalGroupDescription.java
+++ b/java/com/google/gerrit/server/group/InternalGroupDescription.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.GroupDescription;
 import com.google.gerrit.entities.InternalGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 
 public class InternalGroupDescription implements GroupDescription.Internal {
 
@@ -77,7 +77,7 @@
   }
 
   @Override
-  public Timestamp getCreatedOn() {
+  public Instant getCreatedOn() {
     return internalGroup.getCreatedOn();
   }
 
diff --git a/java/com/google/gerrit/server/group/db/AuditLogReader.java b/java/com/google/gerrit/server/group/db/AuditLogReader.java
index d8f0a0f..3f7ef2c 100644
--- a/java/com/google/gerrit/server/group/db/AuditLogReader.java
+++ b/java/com/google/gerrit/server/group/db/AuditLogReader.java
@@ -31,7 +31,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -139,6 +139,9 @@
     return result.stream().map(AccountGroupByIdAudit.Builder::build).collect(toImmutableList());
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private Optional<ParsedCommit> parse(AccountGroup.UUID uuid, RevCommit c) {
     Optional<Account.Id> authorId = NoteDbUtil.parseIdent(c.getAuthorIdent());
     if (!authorId.isPresent()) {
@@ -166,7 +169,7 @@
     return Optional.of(
         new AutoValue_AuditLogReader_ParsedCommit(
             authorId.get(),
-            new Timestamp(c.getAuthorIdent().getWhen().getTime()),
+            c.getAuthorIdent().getWhen().toInstant(),
             ImmutableList.copyOf(addedMembers),
             ImmutableList.copyOf(removedMembers),
             ImmutableList.copyOf(addedSubgroups),
@@ -257,7 +260,7 @@
   abstract static class ParsedCommit {
     abstract Account.Id authorId();
 
-    abstract Timestamp when();
+    abstract Instant when();
 
     abstract ImmutableList<Account.Id> addedMembers();
 
diff --git a/java/com/google/gerrit/server/group/db/GroupConfig.java b/java/com/google/gerrit/server/group/db/GroupConfig.java
index c187186..71cc08c 100644
--- a/java/com/google/gerrit/server/group/db/GroupConfig.java
+++ b/java/com/google/gerrit/server/group/db/GroupConfig.java
@@ -35,8 +35,9 @@
 import com.google.gerrit.server.git.meta.VersionedMetaData;
 import com.google.gerrit.server.util.time.TimeUtil;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.Optional;
 import java.util.function.Function;
 import java.util.regex.Pattern;
@@ -279,7 +280,7 @@
       rw.markStart(revision);
       rw.sort(RevSort.REVERSE);
       RevCommit earliestCommit = rw.next();
-      Timestamp createdOn = new Timestamp(earliestCommit.getCommitTime() * 1000L);
+      Instant createdOn = Instant.ofEpochSecond(earliestCommit.getCommitTime());
 
       Config config = readConfig(GROUP_CONFIG_FILE);
       ImmutableSet<Account.Id> members = readMembers();
@@ -299,6 +300,9 @@
     return c;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Override
   protected boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException {
     checkLoaded();
@@ -314,11 +318,11 @@
 
     // Commit timestamps are internally truncated to seconds. To return the correct 'createdOn' time
     // for new groups, we explicitly need to truncate the timestamp here.
-    Timestamp commitTimestamp =
+    Instant commitTimestamp =
         TimeUtil.truncateToSecond(
-            groupDelta.flatMap(GroupDelta::getUpdatedOn).orElseGet(TimeUtil::nowTs));
-    commit.setAuthor(new PersonIdent(commit.getAuthor(), commitTimestamp));
-    commit.setCommitter(new PersonIdent(commit.getCommitter(), commitTimestamp));
+            groupDelta.flatMap(GroupDelta::getUpdatedOn).orElseGet(TimeUtil::now));
+    commit.setAuthor(new PersonIdent(commit.getAuthor(), Date.from(commitTimestamp)));
+    commit.setCommitter(new PersonIdent(commit.getCommitter(), Date.from(commitTimestamp)));
 
     InternalGroup updatedGroup = updateGroup(commitTimestamp);
 
@@ -346,7 +350,7 @@
     return Optional.empty();
   }
 
-  private InternalGroup updateGroup(Timestamp commitTimestamp)
+  private InternalGroup updateGroup(Instant commitTimestamp)
       throws IOException, ConfigInvalidException {
     Config config = updateGroupProperties();
 
@@ -358,7 +362,7 @@
         loadedGroup.map(InternalGroup::getSubgroups).orElseGet(ImmutableSet::of);
     Optional<ImmutableSet<AccountGroup.UUID>> updatedSubgroups = updateSubgroups(originalSubgroups);
 
-    Timestamp createdOn = loadedGroup.map(InternalGroup::getCreatedOn).orElse(commitTimestamp);
+    Instant createdOn = loadedGroup.map(InternalGroup::getCreatedOn).orElse(commitTimestamp);
 
     return createFrom(
         groupUuid,
@@ -453,7 +457,7 @@
       Config config,
       ImmutableSet<Account.Id> members,
       ImmutableSet<AccountGroup.UUID> subgroups,
-      Timestamp createdOn,
+      Instant createdOn,
       ObjectId refState)
       throws ConfigInvalidException {
     InternalGroup.Builder group = InternalGroup.builder();
diff --git a/java/com/google/gerrit/server/group/db/GroupDelta.java b/java/com/google/gerrit/server/group/db/GroupDelta.java
index 69cb936..ad9c8bd 100644
--- a/java/com/google/gerrit/server/group/db/GroupDelta.java
+++ b/java/com/google/gerrit/server/group/db/GroupDelta.java
@@ -18,7 +18,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.AccountGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 import java.util.Set;
 
@@ -107,7 +107,7 @@
    * in the audit log. For this reason, specifying this field will have an effect on the resulting
    * audit log.
    */
-  public abstract Optional<Timestamp> getUpdatedOn();
+  public abstract Optional<Instant> getUpdatedOn();
 
   public abstract Builder toBuilder();
 
@@ -184,12 +184,12 @@
     public abstract SubgroupModification getSubgroupModification();
 
     /**
-     * Defines the {@code Timestamp} to be used for the NoteDb commits of the update. If not
-     * specified, the current {@code Timestamp} when creating the commit will be used.
+     * Defines the {@code Instant} to be used for the NoteDb commits of the update. If not
+     * specified, the current {@code Instant} when creating the commit will be used.
      *
      * <p>See {@link #getUpdatedOn()}
      */
-    public abstract Builder setUpdatedOn(Timestamp timestamp);
+    public abstract Builder setUpdatedOn(Instant timestamp);
 
     public abstract GroupDelta build();
   }
diff --git a/java/com/google/gerrit/server/group/db/GroupsUpdate.java b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
index 9aa5cfd..c0c934b 100644
--- a/java/com/google/gerrit/server/group/db/GroupsUpdate.java
+++ b/java/com/google/gerrit/server/group/db/GroupsUpdate.java
@@ -50,7 +50,7 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
@@ -248,7 +248,7 @@
   }
 
   private static PersonIdent createPersonIdent(PersonIdent ident, IdentifiedUser user) {
-    return user.newCommitterIdent(ident.getWhen(), ident.getTimeZone());
+    return user.newCommitterIdent(ident);
   }
 
   /**
@@ -292,9 +292,9 @@
     try (TraceTimer ignored =
         TraceContext.newTimer(
             "Updating group", Metadata.builder().groupUuid(groupUuid.get()).build())) {
-      Optional<Timestamp> updatedOn = groupDelta.getUpdatedOn();
+      Optional<Instant> updatedOn = groupDelta.getUpdatedOn();
       if (!updatedOn.isPresent()) {
-        updatedOn = Optional.of(TimeUtil.nowTs());
+        updatedOn = Optional.of(TimeUtil.now());
         groupDelta = groupDelta.toBuilder().setUpdatedOn(updatedOn.get()).build();
       }
 
@@ -505,7 +505,7 @@
     }
   }
 
-  private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Timestamp updatedOn) {
+  private void dispatchAuditEventsOnGroupUpdate(UpdateResult result, Instant updatedOn) {
     if (!currentUser.isPresent()) {
       return;
     }
diff --git a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
index 8a1221e..f422f6a 100644
--- a/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
+++ b/java/com/google/gerrit/server/group/testing/InternalGroupSubject.java
@@ -24,7 +24,7 @@
 import com.google.common.truth.Subject;
 import com.google.gerrit.entities.AccountGroup;
 import com.google.gerrit.entities.InternalGroup;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.eclipse.jgit.lib.ObjectId;
 
 public class InternalGroupSubject extends Subject {
@@ -79,7 +79,7 @@
     return check("isVisibleToAll()").that(group.isVisibleToAll());
   }
 
-  public ComparableSubject<Timestamp> createdOn() {
+  public ComparableSubject<Instant> createdOn() {
     isNotNull();
     return check("getCreatedOn()").that(group.getCreatedOn());
   }
diff --git a/java/com/google/gerrit/server/index/account/AccountField.java b/java/com/google/gerrit/server/index/account/AccountField.java
index 0dd22ce..416b175 100644
--- a/java/com/google/gerrit/server/index/account/AccountField.java
+++ b/java/com/google/gerrit/server/index/account/AccountField.java
@@ -116,8 +116,9 @@
   public static final FieldDef<AccountState, String> PREFERRED_EMAIL_EXACT =
       exact("preferredemail_exact").build(a -> a.account().preferredEmail());
 
+  // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
   public static final FieldDef<AccountState, Timestamp> REGISTERED =
-      timestamp("registered").build(a -> a.account().registeredOn());
+      timestamp("registered").build(a -> Timestamp.from(a.account().registeredOn()));
 
   public static final FieldDef<AccountState, String> USERNAME =
       exact("username").build(a -> a.userName().map(String::toLowerCase).orElse(""));
diff --git a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
index 3f71d39..9f14926 100644
--- a/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
+++ b/java/com/google/gerrit/server/index/change/AllChangesIndexer.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server.index.change;
 
-import static com.google.common.base.Preconditions.checkState;
 import static com.google.common.util.concurrent.Futures.successfulAsList;
 import static com.google.common.util.concurrent.Futures.transform;
 import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
@@ -22,9 +21,8 @@
 
 import com.google.auto.value.AutoValue;
 import com.google.common.base.Stopwatch;
-import com.google.common.collect.Sets;
+import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.flogger.FluentLogger;
-import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.common.util.concurrent.UncheckedExecutionException;
@@ -35,6 +33,7 @@
 import com.google.gerrit.server.git.MultiProgressMonitor;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.git.MultiProgressMonitor.TaskKind;
+import com.google.gerrit.server.git.MultiProgressMonitor.VolatileTask;
 import com.google.gerrit.server.index.IndexExecutor;
 import com.google.gerrit.server.index.OnlineReindexMode;
 import com.google.gerrit.server.notedb.ChangeNotes;
@@ -45,18 +44,13 @@
 import com.google.inject.Inject;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
 import java.util.concurrent.RejectedExecutionException;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.stream.Collectors;
 import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
 
 /**
  * Implementation that can index all changes on a host or within a project. Used by Gerrit's
@@ -65,6 +59,9 @@
  */
 public class AllChangesIndexer extends SiteIndexer<Change.Id, ChangeData, ChangeIndex> {
   private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+  private MultiProgressMonitor mpm;
+  private VolatileTask doneTask;
+  private Task failedTask;
   private static final int PROJECT_SLICE_MAX_REFS = 1000;
 
   private final MultiProgressMonitor.Factory multiProgressMonitorFactory;
@@ -135,56 +132,18 @@
     // in 2020.
 
     Stopwatch sw = Stopwatch.createStarted();
-    List<ProjectSlice> projectSlices;
+    AtomicBoolean ok = new AtomicBoolean(true);
+    mpm = multiProgressMonitorFactory.create(progressOut, TaskKind.INDEXING, "Reindexing changes");
+    doneTask = mpm.beginVolatileSubTask("changes");
+    failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
+    List<ListenableFuture<?>> futures;
     try {
-      projectSlices = new SliceCreator().create();
-    } catch (ProjectsCollectionFailure | InterruptedException | ExecutionException e) {
+      futures = new SliceScheduler(index, ok).schedule();
+    } catch (ProjectsCollectionFailure e) {
       logger.atSevere().log(e.getMessage());
       return Result.create(sw, false, 0, 0);
     }
 
-    // Since project slices are created in parallel, they are somewhat shuffled already. However,
-    // the number of threads used to create the project slices doesn't guarantee good randomization.
-    // If the slices are not shuffled well, then multiple threads would typically work concurrently
-    // on different slices of the same project. While this is not a big issue, shuffling the list
-    // beforehand helps with ungrouping the project slices, so different slices are less likely to
-    // be worked on concurrently.
-    // This shuffling gave a 6% runtime reduction for Wikimedia's Gerrit in 2020.
-    Collections.shuffle(projectSlices);
-    return indexAll(index, projectSlices);
-  }
-
-  private SiteIndexer.Result indexAll(ChangeIndex index, List<ProjectSlice> projectSlices) {
-    Stopwatch sw = Stopwatch.createStarted();
-    MultiProgressMonitor mpm =
-        multiProgressMonitorFactory.create(progressOut, TaskKind.INDEXING, "Reindexing changes");
-    Task projTask = mpm.beginSubTask("project-slices", projectSlices.size());
-    checkState(totalWork >= 0);
-    Task doneTask = mpm.beginSubTask(null, totalWork);
-    Task failedTask = mpm.beginSubTask("failed", MultiProgressMonitor.UNKNOWN);
-
-    List<ListenableFuture<?>> futures = new ArrayList<>();
-    AtomicBoolean ok = new AtomicBoolean(true);
-
-    for (ProjectSlice projectSlice : projectSlices) {
-      Project.NameKey name = projectSlice.name();
-      int slice = projectSlice.slice();
-      int slices = projectSlice.slices();
-      ListenableFuture<?> future =
-          executor.submit(
-              reindexProject(
-                  indexerFactory.create(executor, index),
-                  name,
-                  slice,
-                  slices,
-                  projectSlice.scanResult(),
-                  doneTask,
-                  failedTask));
-      String description = "project " + name + " (" + slice + "/" + slices + ")";
-      addErrorListener(future, description, projTask, ok);
-      futures.add(future);
-    }
-
     try {
       mpm.waitFor(
           transform(
@@ -316,30 +275,53 @@
     }
   }
 
-  private class SliceCreator {
-    final Set<ProjectSlice> projectSlices = Sets.newConcurrentHashSet();
+  private class SliceScheduler {
+    final ChangeIndex index;
+    final AtomicBoolean ok;
     final AtomicInteger changeCount = new AtomicInteger(0);
     final AtomicInteger projectsFailed = new AtomicInteger(0);
-    final ProgressMonitor pm = new TextProgressMonitor();
+    final List<ListenableFuture<?>> sliceIndexerFutures = new ArrayList<>();
+    final List<ListenableFuture<?>> sliceCreationFutures = new ArrayList<>();
+    VolatileTask projTask = mpm.beginVolatileSubTask("project-slices");
+    Task slicingProjects;
 
-    private List<ProjectSlice> create()
-        throws ProjectsCollectionFailure, InterruptedException, ExecutionException {
-      List<ListenableFuture<?>> futures = new ArrayList<>();
-      pm.beginTask("Collecting projects", ProgressMonitor.UNKNOWN);
-      for (Project.NameKey name : projectCache.all()) {
-        futures.add(executor.submit(new ProjectSliceCreator(name)));
+    public SliceScheduler(ChangeIndex index, AtomicBoolean ok) {
+      this.index = index;
+      this.ok = ok;
+    }
+
+    private List<ListenableFuture<?>> schedule() throws ProjectsCollectionFailure {
+      ImmutableSortedSet<Project.NameKey> projects = projectCache.all();
+      int projectCount = projects.size();
+      slicingProjects = mpm.beginSubTask("Slicing projects", projectCount);
+      for (Project.NameKey name : projects) {
+        sliceCreationFutures.add(executor.submit(new ProjectSliceCreator(name)));
       }
 
-      Futures.allAsList(futures).get();
+      try {
+        mpm.waitForNonFinalTask(
+            transform(
+                successfulAsList(sliceCreationFutures),
+                x -> {
+                  projTask.finalizeTotal();
+                  doneTask.finalizeTotal();
+                  return null;
+                },
+                directExecutor()));
+      } catch (UncheckedExecutionException e) {
+        logger.atSevere().withCause(e).log("Error project slice creation");
+        ok.set(false);
+      }
 
-      if (projectsFailed.get() > projectCache.all().size() / 2) {
+      if (projectsFailed.get() > projectCount / 2) {
         throw new ProjectsCollectionFailure(
             "Over 50%% of the projects could not be collected: aborted");
       }
 
-      pm.endTask();
+      slicingProjects.endTask();
       setTotalWork(changeCount.get());
-      return projectSlices.stream().collect(Collectors.toList());
+
+      return sliceIndexerFutures;
     }
 
     private class ProjectSliceCreator implements Callable<Void> {
@@ -361,15 +343,32 @@
               verboseWriter.println(
                   "Submitting " + name + " for indexing in " + slices + " slices");
             }
+
+            doneTask.updateTotal(size);
+            projTask.updateTotal(slices);
+
             for (int slice = 0; slice < slices; slice++) {
-              projectSlices.add(ProjectSlice.create(name, slice, slices, sr));
+              ProjectSlice projectSlice = ProjectSlice.create(name, slice, slices, sr);
+              ListenableFuture<?> future =
+                  executor.submit(
+                      reindexProject(
+                          indexerFactory.create(executor, index),
+                          name,
+                          slice,
+                          slices,
+                          projectSlice.scanResult(),
+                          doneTask,
+                          failedTask));
+              String description = "project " + name + " (" + slice + "/" + slices + ")";
+              addErrorListener(future, description, projTask, ok);
+              sliceIndexerFutures.add(future);
             }
           }
         } catch (IOException e) {
           logger.atSevere().withCause(e).log("Error collecting project %s", name);
           projectsFailed.incrementAndGet();
         }
-        pm.update(1);
+        slicingProjects.update(1);
         return null;
       }
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeField.java b/java/com/google/gerrit/server/index/change/ChangeField.java
index 6c8e3fb..3d5dca8 100644
--- a/java/com/google/gerrit/server/index/change/ChangeField.java
+++ b/java/com/google/gerrit/server/index/change/ChangeField.java
@@ -152,19 +152,29 @@
   public static final FieldDef<ChangeData, String> FUZZY_TOPIC =
       fullText("topic5").build(ChangeField::getTopic);
 
+  /** Topic, a short annotation on the branch. */
+  public static final FieldDef<ChangeData, String> PREFIX_TOPIC =
+      prefix("topic6").build(ChangeField::getTopic);
+
   /** Submission id assigned by MergeOp. */
   public static final FieldDef<ChangeData, String> SUBMISSIONID =
       exact(ChangeQueryBuilder.FIELD_SUBMISSIONID).build(changeGetter(Change::getSubmissionId));
 
   /** Last update time since January 1, 1970. */
+  // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
   public static final FieldDef<ChangeData, Timestamp> UPDATED =
-      timestamp("updated2").stored().build(changeGetter(Change::getLastUpdatedOn));
+      timestamp("updated2")
+          .stored()
+          .build(changeGetter(change -> Timestamp.from(change.getLastUpdatedOn())));
 
   /** When this change was merged, time since January 1, 1970. */
+  // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
   public static final FieldDef<ChangeData, Timestamp> MERGED_ON =
       timestamp(ChangeQueryBuilder.FIELD_MERGED_ON)
           .stored()
-          .build(cd -> cd.getMergedOn().orElse(null), (cd, field) -> cd.setMergedOn(field));
+          .build(
+              cd -> cd.getMergedOn().map(Timestamp::from).orElse(null),
+              (cd, field) -> cd.setMergedOn(field != null ? field.toInstant() : null));
 
   /** List of full file paths modified in the current patch set. */
   public static final FieldDef<ChangeData, Iterable<String>> PATH =
@@ -195,6 +205,11 @@
       fullText("hashtag2")
           .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
 
+  /** Hashtags as prefix field for in-string search. */
+  public static final FieldDef<ChangeData, Iterable<String>> PREFIX_HASHTAG =
+      prefix("hashtag3")
+          .buildRepeatable(cd -> cd.hashtags().stream().map(String::toLowerCase).collect(toSet()));
+
   /** Hashtags with original case. */
   public static final FieldDef<ChangeData, Iterable<byte[]>> HASHTAG_CASE_AWARE =
       storedOnly("_hashtag")
@@ -435,11 +450,10 @@
   @VisibleForTesting
   static List<String> getReviewerFieldValues(ReviewerSet reviewers) {
     List<String> r = new ArrayList<>(reviewers.asTable().size() * 2);
-    for (Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c :
-        reviewers.asTable().cellSet()) {
+    for (Table.Cell<ReviewerStateInternal, Account.Id, Instant> c : reviewers.asTable().cellSet()) {
       String v = getReviewerFieldValue(c.getRowKey(), c.getColumnKey());
       r.add(v);
-      r.add(v + ',' + c.getValue().getTime());
+      r.add(v + ',' + c.getValue().toEpochMilli());
     }
     return r;
   }
@@ -451,7 +465,7 @@
   @VisibleForTesting
   static List<String> getReviewerByEmailFieldValues(ReviewerByEmailSet reviewersByEmail) {
     List<String> r = new ArrayList<>(reviewersByEmail.asTable().size() * 2);
-    for (Table.Cell<ReviewerStateInternal, Address, Timestamp> c :
+    for (Table.Cell<ReviewerStateInternal, Address, Instant> c :
         reviewersByEmail.asTable().cellSet()) {
       String v = getReviewerByEmailFieldValue(c.getRowKey(), c.getColumnKey());
       r.add(v);
@@ -460,7 +474,7 @@
         Address emailOnly = Address.create(c.getColumnKey().email());
         r.add(getReviewerByEmailFieldValue(c.getRowKey(), emailOnly));
       }
-      r.add(v + ',' + c.getValue().getTime());
+      r.add(v + ',' + c.getValue().toEpochMilli());
     }
     return r;
   }
@@ -470,8 +484,7 @@
   }
 
   public static ReviewerSet parseReviewerFieldValues(Change.Id changeId, Iterable<String> values) {
-    ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
-        ImmutableTable.builder();
+    ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b = ImmutableTable.builder();
     for (String v : values) {
 
       int i = v.indexOf(',');
@@ -513,7 +526,7 @@
             "Failed to parse timestamp of reviewer field from change %s: %s", changeId.get(), v);
         continue;
       }
-      Timestamp timestamp = new Timestamp(l);
+      Instant timestamp = Instant.ofEpochMilli(l);
 
       b.put(reviewerState.get(), accountId.get(), timestamp);
     }
@@ -522,7 +535,7 @@
 
   public static ReviewerByEmailSet parseReviewerByEmailFieldValues(
       Change.Id changeId, Iterable<String> values) {
-    ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b = ImmutableTable.builder();
+    ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
     for (String v : values) {
       int i = v.indexOf(',');
       if (i < 0) {
@@ -566,7 +579,7 @@
             changeId.get(), v);
         continue;
       }
-      Timestamp timestamp = new Timestamp(l);
+      Instant timestamp = Instant.ofEpochMilli(l);
 
       b.put(reviewerState.get(), address, timestamp);
     }
diff --git a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
index e1536cc..9ff806d 100644
--- a/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
+++ b/java/com/google/gerrit/server/index/change/ChangeSchemaDefinitions.java
@@ -196,11 +196,23 @@
   /** Added new "count=$count" argument to the {@link ChangeField#LABEL} operator. */
   static final Schema<ChangeData> V73 = schema(V72, false);
 
+  @Deprecated
   /** Added new field {@link ChangeField#IS_SUBMITTABLE} based on submit requirements. */
   static final Schema<ChangeData> V74 =
       new Schema.Builder<ChangeData>().add(V73).add(ChangeField.IS_SUBMITTABLE).build();
 
   /**
+   * Added new field {@link ChangeField#PREFIX_HASHTAG} and {@link ChangeField#PREFIX_TOPIC} to
+   * allow easier search for topics.
+   */
+  static final Schema<ChangeData> V75 =
+      new Schema.Builder<ChangeData>()
+          .add(V74)
+          .add(ChangeField.PREFIX_HASHTAG)
+          .add(ChangeField.PREFIX_TOPIC)
+          .build();
+
+  /**
    * Name of the change index to be used when contacting index backends or loading configurations.
    */
   public static final String NAME = "changes";
diff --git a/java/com/google/gerrit/server/index/group/GroupField.java b/java/com/google/gerrit/server/index/group/GroupField.java
index df90c0d..af74514 100644
--- a/java/com/google/gerrit/server/index/group/GroupField.java
+++ b/java/com/google/gerrit/server/index/group/GroupField.java
@@ -47,8 +47,9 @@
       exact("owner_uuid").build(g -> g.getOwnerGroupUUID().get());
 
   /** Timestamp indicating when this group was created. */
+  // TODO(issue-15518): Migrate type for timestamp index fields from Timestamp to Instant
   public static final FieldDef<InternalGroup, Timestamp> CREATED_ON =
-      timestamp("created_on").build(InternalGroup::getCreatedOn);
+      timestamp("created_on").build(internalGroup -> Timestamp.from(internalGroup.getCreatedOn()));
 
   /** Group name. */
   public static final FieldDef<InternalGroup, String> NAME =
diff --git a/java/com/google/gerrit/server/logging/RequestId.java b/java/com/google/gerrit/server/logging/RequestId.java
index 543f0a2..3ae9598 100644
--- a/java/com/google/gerrit/server/logging/RequestId.java
+++ b/java/com/google/gerrit/server/logging/RequestId.java
@@ -61,7 +61,7 @@
     h.putLong(Thread.currentThread().getId()).putUnencodedChars(MACHINE_ID);
     str =
         (resourceId != null ? resourceId + "-" : "")
-            + TimeUtil.nowTs().getTime()
+            + TimeUtil.now().toEpochMilli()
             + "-"
             + h.hash().toString().substring(0, 8);
   }
diff --git a/java/com/google/gerrit/server/mail/receive/MailProcessor.java b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
index 6d20ae5..0fc89ba 100644
--- a/java/com/google/gerrit/server/mail/receive/MailProcessor.java
+++ b/java/com/google/gerrit/server/mail/receive/MailProcessor.java
@@ -313,7 +313,7 @@
       }
 
       Op o = new Op(PatchSet.id(cd.getId(), metadata.patchSet), parsedComments, message.id());
-      BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.nowTs());
+      BatchUpdate batchUpdate = buf.create(project, ctx.getUser(), TimeUtil.now());
       batchUpdate.addOp(cd.getId(), o);
       batchUpdate.execute();
     }
diff --git a/java/com/google/gerrit/server/mail/send/ChangeEmail.java b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
index 9aec1f4..fd79457 100644
--- a/java/com/google/gerrit/server/mail/send/ChangeEmail.java
+++ b/java/com/google/gerrit/server/mail/send/ChangeEmail.java
@@ -52,10 +52,9 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.change.ChangeData;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.Collection;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -87,7 +86,7 @@
   protected PatchSet patchSet;
   protected PatchSetInfo patchSetInfo;
   protected String changeMessage;
-  protected Timestamp timestamp;
+  protected Instant timestamp;
 
   protected ProjectState projectState;
   protected Set<Account.Id> authors;
@@ -124,7 +123,7 @@
     patchSetInfo = psi;
   }
 
-  public void setChangeMessage(String cm, Timestamp t) {
+  public void setChangeMessage(String cm, Instant t) {
     changeMessage = cm;
     timestamp = t;
   }
@@ -191,7 +190,7 @@
 
     super.init();
     if (timestamp != null) {
-      setHeader(FieldName.DATE, new Date(timestamp.getTime()));
+      setHeader(FieldName.DATE, timestamp);
     }
     setChangeSubjectHeader();
     setHeader(MailHeader.CHANGE_ID.fieldName(), "" + change.getKey().get());
@@ -241,7 +240,7 @@
 
   public String getChangeMessageThreadId() {
     return "<gerrit."
-        + change.getCreatedOn().getTime()
+        + change.getCreatedOn().toEpochMilli()
         + "."
         + change.getKey().get()
         + "@"
diff --git a/java/com/google/gerrit/server/mail/send/CommentSender.java b/java/com/google/gerrit/server/mail/send/CommentSender.java
index 5a7352a..81e4f3e 100644
--- a/java/com/google/gerrit/server/mail/send/CommentSender.java
+++ b/java/com/google/gerrit/server/mail/send/CommentSender.java
@@ -555,6 +555,6 @@
   private String getCommentTimestamp() {
     // Grouping is currently done by timestamp.
     return MailProcessingUtil.rfcDateformatter.format(
-        ZonedDateTime.ofInstant(timestamp.toInstant(), ZoneId.of("UTC")));
+        ZonedDateTime.ofInstant(timestamp, ZoneId.of("UTC")));
   }
 }
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index d2e878f..32a8252 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -42,9 +42,9 @@
 import com.google.template.soy.jbcsrc.api.SoySauce;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -324,7 +324,7 @@
     setupSoyContext();
 
     smtpFromAddress = args.fromAddressGenerator.get().from(fromId);
-    setHeader(FieldName.DATE, new Date());
+    setHeader(FieldName.DATE, Instant.now());
     headers.put(FieldName.FROM, new EmailHeader.AddressList(smtpFromAddress));
     headers.put(FieldName.TO, new EmailHeader.AddressList());
     headers.put(FieldName.CC, new EmailHeader.AddressList());
@@ -398,7 +398,7 @@
     headers.remove(name);
   }
 
-  protected void setHeader(String name, Date date) {
+  protected void setHeader(String name, Instant date) {
     headers.put(name, new EmailHeader.Date(date));
   }
 
diff --git a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
index d32e6fb..0a721cf 100644
--- a/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
+++ b/java/com/google/gerrit/server/mail/send/SmtpEmailSender.java
@@ -36,10 +36,11 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.Writer;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.Map;
@@ -281,9 +282,11 @@
       setMissingHeader(hdrs, "Importance", importance);
     }
     if (expiryDays > 0) {
-      Date expiry = new Date(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L);
-      setMissingHeader(
-          hdrs, "Expiry-Date", new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
+      Instant expiry = Instant.ofEpochMilli(TimeUtil.nowMs() + expiryDays * 24 * 60 * 60 * 1000L);
+      DateTimeFormatter fmt =
+          DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss Z")
+              .withZone(ZoneId.systemDefault());
+      setMissingHeader(hdrs, "Expiry-Date", fmt.format(expiry));
     }
 
     String encodedBody;
diff --git a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
index 6677490..7efda47 100644
--- a/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/AbstractChangeUpdate.java
@@ -28,6 +28,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.Date;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
@@ -45,7 +46,7 @@
   protected final Account.Id accountId;
   protected final Account.Id realAccountId;
   protected final PersonIdent authorIdent;
-  protected final Date when;
+  protected final Instant when;
 
   @Nullable private final ChangeNotes notes;
   private final Change change;
@@ -55,14 +56,17 @@
   private ObjectId result;
   boolean rootOnly;
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   AbstractChangeUpdate(
       ChangeNotes notes,
       CurrentUser user,
       PersonIdent serverIdent,
       ChangeNoteUtil noteUtil,
-      Date when) {
+      Instant when) {
     this.noteUtil = noteUtil;
-    this.serverIdent = new PersonIdent(serverIdent, when);
+    this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
     this.notes = notes;
     this.change = notes.getChange();
     this.accountId = accountId(user);
@@ -72,6 +76,9 @@
     this.when = when;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   AbstractChangeUpdate(
       ChangeNoteUtil noteUtil,
       PersonIdent serverIdent,
@@ -80,12 +87,12 @@
       Account.Id accountId,
       Account.Id realAccountId,
       PersonIdent authorIdent,
-      Date when) {
+      Instant when) {
     checkArgument(
         (notes != null && change == null) || (notes == null && change != null),
         "exactly one of notes or change required");
     this.noteUtil = noteUtil;
-    this.serverIdent = new PersonIdent(serverIdent, when);
+    this.serverIdent = new PersonIdent(serverIdent, Date.from(when));
     this.notes = notes;
     this.change = change != null ? change : notes.getChange();
     this.accountId = accountId;
@@ -107,7 +114,7 @@
   }
 
   private static PersonIdent ident(
-      ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Date when) {
+      ChangeNoteUtil noteUtil, PersonIdent serverIdent, CurrentUser u, Instant when) {
     checkUserType(u);
     if (u instanceof IdentifiedUser) {
       return noteUtil.newAccountIdIdent(u.asIdentifiedUser().getAccount().id(), when, serverIdent);
@@ -137,7 +144,7 @@
     return change;
   }
 
-  public Date getWhen() {
+  public Instant getWhen() {
     return when;
   }
 
@@ -206,6 +213,9 @@
    *     deleted.
    * @throws IOException if a lower-level error occurred.
    */
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   final ObjectId apply(RevWalk rw, ObjectInserter ins, ObjectId curr) throws IOException {
     if (isEmpty()) {
       return null;
@@ -226,7 +236,7 @@
       return null; // Impl is a no-op.
     }
     cb.setAuthor(authorIdent);
-    cb.setCommitter(new PersonIdent(serverIdent, when));
+    cb.setCommitter(new PersonIdent(serverIdent, Date.from(when)));
     setParentCommit(cb, curr);
     if (cb.getTreeId() == null) {
       if (curr.equals(z)) {
diff --git a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
index 57f6353..5d19205 100644
--- a/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeDraftUpdate.java
@@ -31,9 +31,9 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -60,14 +60,14 @@
         @Assisted("effective") Account.Id accountId,
         @Assisted("real") Account.Id realAccountId,
         PersonIdent authorIdent,
-        Date when);
+        Instant when);
 
     ChangeDraftUpdate create(
         Change change,
         @Assisted("effective") Account.Id accountId,
         @Assisted("real") Account.Id realAccountId,
         PersonIdent authorIdent,
-        Date when);
+        Instant when);
   }
 
   @AutoValue
@@ -101,7 +101,7 @@
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
-      @Assisted Date when) {
+      @Assisted Instant when) {
     super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
     this.draftsProject = allUsers;
   }
@@ -115,7 +115,7 @@
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
-      @Assisted Date when) {
+      @Assisted Instant when) {
     super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
     this.draftsProject = allUsers;
   }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
index 4f72f2e..e9d2f4c 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNoteUtil.java
@@ -100,11 +100,15 @@
    * Returns a {@link PersonIdent} that contains the account ID, but not the user's name or email
    * address.
    */
-  public PersonIdent newAccountIdIdent(Account.Id accountId, Date when, PersonIdent serverIdent) {
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  public PersonIdent newAccountIdIdent(
+      Account.Id accountId, Instant when, PersonIdent serverIdent) {
     return new PersonIdent(
         getAccountIdAsUsername(accountId),
         getAccountIdAsEmailAddress(accountId),
-        when,
+        Date.from(when),
         serverIdent.getTimeZone());
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotes.java b/java/com/google/gerrit/server/notedb/ChangeNotes.java
index 0c6f518..bec4b721f 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotes.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotes.java
@@ -66,7 +66,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -567,7 +567,7 @@
   }
 
   /** Returns {@link Optional} value of time when the change was merged. */
-  public Optional<Timestamp> getMergedOn() {
+  public Optional<Instant> getMergedOn() {
     return Optional.ofNullable(state.mergedOn());
   }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 463c141..8c876b4 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -125,8 +125,8 @@
 
   // Private final but mutable members initialized in the constructor and filled
   // in during the parsing process.
-  private final Table<Account.Id, ReviewerStateInternal, Timestamp> reviewers;
-  private final Table<Address, ReviewerStateInternal, Timestamp> reviewersByEmail;
+  private final Table<Account.Id, ReviewerStateInternal, Instant> reviewers;
+  private final Table<Address, ReviewerStateInternal, Instant> reviewersByEmail;
   private final List<Account.Id> allPastReviewers;
   private final List<ReviewerStatusUpdate> reviewerUpdates;
   /** Holds only the most recent update per user. Older updates are discarded. */
@@ -151,8 +151,8 @@
   private Change.Status status;
   private String topic;
   private Set<String> hashtags;
-  private Timestamp createdOn;
-  private Timestamp lastUpdatedOn;
+  private Instant createdOn;
+  private Instant lastUpdatedOn;
   private Account.Id ownerId;
   private String serverId;
   private String changeId;
@@ -173,7 +173,7 @@
   // We only set the value once, based on the latest update (the actual value or Optional.empty() if
   // the latest record unsets the field).
   private Optional<PatchSet.Id> cherryPickOf;
-  private Timestamp mergedOn;
+  private Instant mergedOn;
 
   ChangeNotesParser(
       Change.Id changeId,
@@ -413,7 +413,7 @@
   }
 
   private void parse(ChangeNotesCommit commit) throws ConfigInvalidException {
-    Timestamp commitTimestamp = getCommitTimestamp(commit);
+    Instant commitTimestamp = getCommitTimestamp(commit);
 
     createdOn = commitTimestamp;
     parseTag(commit);
@@ -467,7 +467,7 @@
 
     parseSubmission(commit, commitTimestamp);
 
-    if (lastUpdatedOn == null || commitTimestamp.after(lastUpdatedOn)) {
+    if (lastUpdatedOn == null || commitTimestamp.isAfter(lastUpdatedOn)) {
       lastUpdatedOn = commitTimestamp;
     }
 
@@ -529,7 +529,7 @@
     }
   }
 
-  private void parseSubmission(ChangeNotesCommit commit, Timestamp commitTimestamp)
+  private void parseSubmission(ChangeNotesCommit commit, Instant commitTimestamp)
       throws ConfigInvalidException {
     // Only parse the most recent sumbit commit (there should be exactly one).
     if (submissionId == null) {
@@ -612,7 +612,7 @@
     }
   }
 
-  private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Timestamp ts)
+  private void parsePatchSet(PatchSet.Id psId, ObjectId rev, Account.Id accountId, Instant ts)
       throws ConfigInvalidException {
     if (accountId == null) {
       throw parseException("patch set %s requires an identified user as uploader", psId.get());
@@ -707,7 +707,7 @@
     }
   }
 
-  private void parseAssigneeUpdates(Timestamp ts, ChangeNotesCommit commit)
+  private void parseAssigneeUpdates(Instant ts, ChangeNotesCommit commit)
       throws ConfigInvalidException {
     String assigneeValue = parseOneFooter(commit, FOOTER_ASSIGNEE);
     if (assigneeValue != null) {
@@ -817,7 +817,7 @@
       Account.Id accountId,
       Account.Id realAccountId,
       ChangeNotesCommit commit,
-      Timestamp ts) {
+      Instant ts) {
     Optional<String> changeMsgString = getChangeMessageString(commit);
     if (!changeMsgString.isPresent()) {
       return false;
@@ -902,7 +902,7 @@
   }
 
   /** Parses copied {@link PatchSetApproval}. */
-  private void parseCopiedApproval(PatchSet.Id psId, Timestamp ts, String line)
+  private void parseCopiedApproval(PatchSet.Id psId, Instant ts, String line)
       throws ConfigInvalidException {
     ParsedPatchSetApproval parsedPatchSetApproval = ChangeNoteUtil.parseCopiedApproval(line);
     checkFooter(
@@ -950,7 +950,7 @@
   }
 
   private void parseApproval(
-      PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Timestamp ts, String line)
+      PatchSet.Id psId, Account.Id accountId, Account.Id realAccountId, Instant ts, String line)
       throws ConfigInvalidException {
     if (accountId == null) {
       throw parseException("patch set %s requires an identified user as uploader", psId.get());
@@ -970,7 +970,7 @@
       PatchSet.Id psId,
       Account.Id committerId,
       Account.Id realAccountId,
-      Timestamp ts,
+      Instant ts,
       ParsedPatchSetApproval parsedPatchSetApproval)
       throws ConfigInvalidException {
 
@@ -1004,7 +1004,7 @@
       PatchSet.Id psId,
       Account.Id committerId,
       Account.Id realAccountId,
-      Timestamp ts,
+      Instant ts,
       ParsedPatchSetApproval parsedPatchSetApproval)
       throws ConfigInvalidException {
 
@@ -1118,7 +1118,7 @@
     return parseIdent(a);
   }
 
-  private void parseReviewer(Timestamp ts, ReviewerStateInternal state, String line)
+  private void parseReviewer(Instant ts, ReviewerStateInternal state, String line)
       throws ConfigInvalidException {
     PersonIdent ident = RawParseUtils.parsePersonIdent(line);
     if (ident == null) {
@@ -1131,7 +1131,7 @@
     }
   }
 
-  private void parseReviewerByEmail(Timestamp ts, ReviewerStateInternal state, String line)
+  private void parseReviewerByEmail(Instant ts, ReviewerStateInternal state, String line)
       throws ConfigInvalidException {
     Address adr;
     try {
@@ -1245,15 +1245,18 @@
    * @param commit the commit to return commit time.
    * @return the timestamp when the commit was applied.
    */
-  private Timestamp getCommitTimestamp(ChangeNotesCommit commit) {
-    return new Timestamp(commit.getCommitterIdent().getWhen().getTime());
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  private Instant getCommitTimestamp(ChangeNotesCommit commit) {
+    return commit.getCommitterIdent().getWhen().toInstant();
   }
 
   private void pruneReviewers() {
-    Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Timestamp>> rit =
+    Iterator<Table.Cell<Account.Id, ReviewerStateInternal, Instant>> rit =
         reviewers.cellSet().iterator();
     while (rit.hasNext()) {
-      Table.Cell<Account.Id, ReviewerStateInternal, Timestamp> e = rit.next();
+      Table.Cell<Account.Id, ReviewerStateInternal, Instant> e = rit.next();
       if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
         rit.remove();
       }
@@ -1261,10 +1264,10 @@
   }
 
   private void pruneReviewersByEmail() {
-    Iterator<Table.Cell<Address, ReviewerStateInternal, Timestamp>> rit =
+    Iterator<Table.Cell<Address, ReviewerStateInternal, Instant>> rit =
         reviewersByEmail.cellSet().iterator();
     while (rit.hasNext()) {
-      Table.Cell<Address, ReviewerStateInternal, Timestamp> e = rit.next();
+      Table.Cell<Address, ReviewerStateInternal, Instant> e = rit.next();
       if (e.getColumnKey() == ReviewerStateInternal.REMOVED) {
         rit.remove();
       }
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesState.java b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
index 4d6b9cf..b0079d7 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesState.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesState.java
@@ -65,7 +65,6 @@
 import com.google.gerrit.server.cache.serialize.ObjectIdConverter;
 import com.google.gerrit.server.index.change.ChangeField.StoredSubmitRecord;
 import com.google.gson.Gson;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
@@ -103,8 +102,8 @@
       ObjectId metaId,
       Change.Id changeId,
       Change.Key changeKey,
-      Timestamp createdOn,
-      Timestamp lastUpdatedOn,
+      Instant createdOn,
+      Instant lastUpdatedOn,
       Account.Id owner,
       String serverId,
       String branch,
@@ -136,7 +135,7 @@
       @Nullable Change.Id revertOf,
       @Nullable PatchSet.Id cherryPickOf,
       int updateCount,
-      @Nullable Timestamp mergedOn) {
+      @Nullable Instant mergedOn) {
     requireNonNull(
         metaId,
         () ->
@@ -203,9 +202,9 @@
 
     abstract Change.Key changeKey();
 
-    abstract Timestamp createdOn();
+    abstract Instant createdOn();
 
-    abstract Timestamp lastUpdatedOn();
+    abstract Instant lastUpdatedOn();
 
     abstract Account.Id owner();
 
@@ -249,9 +248,9 @@
 
       abstract Builder changeKey(Change.Key changeKey);
 
-      abstract Builder createdOn(Timestamp createdOn);
+      abstract Builder createdOn(Instant createdOn);
 
-      abstract Builder lastUpdatedOn(Timestamp lastUpdatedOn);
+      abstract Builder lastUpdatedOn(Instant lastUpdatedOn);
 
       abstract Builder owner(Account.Id owner);
 
@@ -334,7 +333,7 @@
   abstract int updateCount();
 
   @Nullable
-  abstract Timestamp mergedOn();
+  abstract Instant mergedOn();
 
   Change newChange(Project.NameKey project) {
     ChangeColumns c = requireNonNull(columns(), "columns are required");
@@ -456,7 +455,7 @@
 
     abstract Builder updateCount(int updateCount);
 
-    abstract Builder mergedOn(Timestamp mergedOn);
+    abstract Builder mergedOn(Instant mergedOn);
 
     abstract ChangeNotesState build();
   }
@@ -536,7 +535,7 @@
                       SubmitRequirementProtoConverter.INSTANCE.toProto(sr)));
       b.setUpdateCount(object.updateCount());
       if (object.mergedOn() != null) {
-        b.setMergedOnMillis(object.mergedOn().getTime());
+        b.setMergedOnMillis(object.mergedOn().toEpochMilli());
         b.setHasMergedOn(true);
       }
 
@@ -547,8 +546,8 @@
       ChangeColumnsProto.Builder b =
           ChangeColumnsProto.newBuilder()
               .setChangeKey(cols.changeKey().get())
-              .setCreatedOnMillis(cols.createdOn().getTime())
-              .setLastUpdatedOnMillis(cols.lastUpdatedOn().getTime())
+              .setCreatedOnMillis(cols.createdOn().toEpochMilli())
+              .setLastUpdatedOnMillis(cols.lastUpdatedOn().toEpochMilli())
               .setOwner(cols.owner().get())
               .setBranch(cols.branch());
       if (cols.currentPatchSetId() != null) {
@@ -581,26 +580,26 @@
     }
 
     private static ReviewerSetEntryProto toReviewerSetEntry(
-        Table.Cell<ReviewerStateInternal, Account.Id, Timestamp> c) {
+        Table.Cell<ReviewerStateInternal, Account.Id, Instant> c) {
       return ReviewerSetEntryProto.newBuilder()
           .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
           .setAccountId(c.getColumnKey().get())
-          .setTimestampMillis(c.getValue().getTime())
+          .setTimestampMillis(c.getValue().toEpochMilli())
           .build();
     }
 
     private static ReviewerByEmailSetEntryProto toReviewerByEmailSetEntry(
-        Table.Cell<ReviewerStateInternal, Address, Timestamp> c) {
+        Table.Cell<ReviewerStateInternal, Address, Instant> c) {
       return ReviewerByEmailSetEntryProto.newBuilder()
           .setState(REVIEWER_STATE_CONVERTER.reverse().convert(c.getRowKey()))
           .setAddress(c.getColumnKey().toHeaderString())
-          .setTimestampMillis(c.getValue().getTime())
+          .setTimestampMillis(c.getValue().toEpochMilli())
           .build();
     }
 
     private static ReviewerStatusUpdateProto toReviewerStatusUpdateProto(ReviewerStatusUpdate u) {
       return ReviewerStatusUpdateProto.newBuilder()
-          .setTimestampMillis(u.date().getTime())
+          .setTimestampMillis(u.date().toEpochMilli())
           .setUpdatedBy(u.updatedBy().get())
           .setReviewer(u.reviewer().get())
           .setState(REVIEWER_STATE_CONVERTER.reverse().convert(u.state()))
@@ -620,7 +619,7 @@
     private static AssigneeStatusUpdateProto toAssigneeStatusUpdateProto(AssigneeStatusUpdate u) {
       AssigneeStatusUpdateProto.Builder builder =
           AssigneeStatusUpdateProto.newBuilder()
-              .setTimestampMillis(u.date().getTime())
+              .setTimestampMillis(u.date().toEpochMilli())
               .setUpdatedBy(u.updatedBy().get())
               .setHasCurrentAssignee(u.currentAssignee().isPresent());
 
@@ -678,7 +677,8 @@
                       .map(sr -> SubmitRequirementProtoConverter.INSTANCE.fromProto(sr))
                       .collect(toImmutableList()))
               .updateCount(proto.getUpdateCount())
-              .mergedOn(proto.getHasMergedOn() ? new Timestamp(proto.getMergedOnMillis()) : null);
+              .mergedOn(
+                  proto.getHasMergedOn() ? Instant.ofEpochMilli(proto.getMergedOnMillis()) : null);
       return b.build();
     }
 
@@ -686,8 +686,8 @@
       ChangeColumns.Builder b =
           ChangeColumns.builder()
               .changeKey(Change.key(proto.getChangeKey()))
-              .createdOn(new Timestamp(proto.getCreatedOnMillis()))
-              .lastUpdatedOn(new Timestamp(proto.getLastUpdatedOnMillis()))
+              .createdOn(Instant.ofEpochMilli(proto.getCreatedOnMillis()))
+              .lastUpdatedOn(Instant.ofEpochMilli(proto.getLastUpdatedOnMillis()))
               .owner(Account.id(proto.getOwner()))
               .branch(proto.getBranch());
       if (proto.getHasCurrentPatchSetId()) {
@@ -719,26 +719,25 @@
     }
 
     private static ReviewerSet toReviewerSet(List<ReviewerSetEntryProto> protos) {
-      ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Timestamp> b =
+      ImmutableTable.Builder<ReviewerStateInternal, Account.Id, Instant> b =
           ImmutableTable.builder();
       for (ReviewerSetEntryProto e : protos) {
         b.put(
             REVIEWER_STATE_CONVERTER.convert(e.getState()),
             Account.id(e.getAccountId()),
-            new Timestamp(e.getTimestampMillis()));
+            Instant.ofEpochMilli(e.getTimestampMillis()));
       }
       return ReviewerSet.fromTable(b.build());
     }
 
     private static ReviewerByEmailSet toReviewerByEmailSet(
         List<ReviewerByEmailSetEntryProto> protos) {
-      ImmutableTable.Builder<ReviewerStateInternal, Address, Timestamp> b =
-          ImmutableTable.builder();
+      ImmutableTable.Builder<ReviewerStateInternal, Address, Instant> b = ImmutableTable.builder();
       for (ReviewerByEmailSetEntryProto e : protos) {
         b.put(
             REVIEWER_STATE_CONVERTER.convert(e.getState()),
             Address.parse(e.getAddress()),
-            new Timestamp(e.getTimestampMillis()));
+            Instant.ofEpochMilli(e.getTimestampMillis()));
       }
       return ReviewerByEmailSet.fromTable(b.build());
     }
@@ -749,7 +748,7 @@
       for (ReviewerStatusUpdateProto proto : protos) {
         b.add(
             ReviewerStatusUpdate.create(
-                new Timestamp(proto.getTimestampMillis()),
+                Instant.ofEpochMilli(proto.getTimestampMillis()),
                 Account.id(proto.getUpdatedBy()),
                 Account.id(proto.getReviewer()),
                 REVIEWER_STATE_CONVERTER.convert(proto.getState())));
@@ -791,7 +790,7 @@
       for (AssigneeStatusUpdateProto proto : protos) {
         b.add(
             AssigneeStatusUpdate.create(
-                new Timestamp(proto.getTimestampMillis()),
+                Instant.ofEpochMilli(proto.getTimestampMillis()),
                 Account.id(proto.getUpdatedBy()),
                 proto.getHasCurrentAssignee()
                     ? Optional.of(Account.id(proto.getCurrentAssignee()))
diff --git a/java/com/google/gerrit/server/notedb/ChangeUpdate.java b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
index 4894140..3d7ae10 100644
--- a/java/com/google/gerrit/server/notedb/ChangeUpdate.java
+++ b/java/com/google/gerrit/server/notedb/ChangeUpdate.java
@@ -84,10 +84,10 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Comparator;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -121,10 +121,10 @@
  */
 public class ChangeUpdate extends AbstractChangeUpdate {
   public interface Factory {
-    ChangeUpdate create(ChangeNotes notes, CurrentUser user, Date when);
+    ChangeUpdate create(ChangeNotes notes, CurrentUser user, Instant when);
 
     ChangeUpdate create(
-        ChangeNotes notes, CurrentUser user, Date when, Comparator<String> labelNameComparator);
+        ChangeNotes notes, CurrentUser user, Instant when, Comparator<String> labelNameComparator);
   }
 
   private final NoteDbUpdateManager.Factory updateManagerFactory;
@@ -186,7 +186,7 @@
       PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
       @Assisted ChangeNotes notes,
       @Assisted CurrentUser user,
-      @Assisted Date when,
+      @Assisted Instant when,
       ChangeNoteUtil noteUtil) {
     this(
         serverIdent,
@@ -223,7 +223,7 @@
       PatchSetApprovalUuidGenerator patchSetApprovalUuidGenerator,
       @Assisted ChangeNotes notes,
       @Assisted CurrentUser user,
-      @Assisted Date when,
+      @Assisted Instant when,
       @Assisted Comparator<String> labelNameComparator,
       ChangeNoteUtil noteUtil) {
     super(notes, user, serverIdent, noteUtil, when);
diff --git a/java/com/google/gerrit/server/notedb/CommitRewriter.java b/java/com/google/gerrit/server/notedb/CommitRewriter.java
index 74f7e13..24c4d6d 100644
--- a/java/com/google/gerrit/server/notedb/CommitRewriter.java
+++ b/java/com/google/gerrit/server/notedb/CommitRewriter.java
@@ -577,6 +577,9 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private boolean verifyPersonIdent(PersonIdent newIdent, PersonIdent originalIdent) {
     return newIdent.getTimeZoneOffset() == originalIdent.getTimeZoneOffset()
         && newIdent.getWhen().getTime() == originalIdent.getWhen().getTime()
diff --git a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
index 895f378..edf5bd3 100644
--- a/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
+++ b/java/com/google/gerrit/server/notedb/RobotCommentUpdate.java
@@ -28,9 +28,9 @@
 import com.google.inject.assistedinject.Assisted;
 import com.google.inject.assistedinject.AssistedInject;
 import java.io.IOException;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -57,14 +57,14 @@
         @Assisted("effective") Account.Id accountId,
         @Assisted("real") Account.Id realAccountId,
         PersonIdent authorIdent,
-        Date when);
+        Instant when);
 
     RobotCommentUpdate create(
         Change change,
         @Assisted("effective") Account.Id accountId,
         @Assisted("real") Account.Id realAccountId,
         PersonIdent authorIdent,
-        Date when);
+        Instant when);
   }
 
   private List<RobotComment> put = new ArrayList<>();
@@ -77,7 +77,7 @@
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
-      @Assisted Date when) {
+      @Assisted Instant when) {
     super(noteUtil, serverIdent, notes, null, accountId, realAccountId, authorIdent, when);
   }
 
@@ -89,7 +89,7 @@
       @Assisted("effective") Account.Id accountId,
       @Assisted("real") Account.Id realAccountId,
       @Assisted PersonIdent authorIdent,
-      @Assisted Date when) {
+      @Assisted Instant when) {
     super(noteUtil, serverIdent, null, change, accountId, realAccountId, authorIdent, when);
   }
 
diff --git a/java/com/google/gerrit/server/plugins/JarPluginProvider.java b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
index 82f97c9..c00a69d 100644
--- a/java/com/google/gerrit/server/plugins/JarPluginProvider.java
+++ b/java/com/google/gerrit/server/plugins/JarPluginProvider.java
@@ -29,9 +29,10 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.text.SimpleDateFormat;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -104,8 +105,8 @@
   }
 
   private static String tempNameFor(String name) {
-    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
-    return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
+    DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyMMdd_HHmm").withZone(ZoneId.of("UTC"));
+    return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(Instant.now()) + "_";
   }
 
   public static Path storeInTemp(String pluginName, InputStream in, SitePaths sitePaths)
diff --git a/java/com/google/gerrit/server/query/change/AgePredicate.java b/java/com/google/gerrit/server/query/change/AgePredicate.java
index d2c6e6f..c1138bd 100644
--- a/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -36,7 +36,7 @@
 
   @Override
   public Instant getMinTimestamp() {
-    return Instant.ofEpochMilli(0);
+    return Instant.EPOCH;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/BeforePredicate.java b/java/com/google/gerrit/server/query/change/BeforePredicate.java
index 61bf6b1..5d682fb 100644
--- a/java/com/google/gerrit/server/query/change/BeforePredicate.java
+++ b/java/com/google/gerrit/server/query/change/BeforePredicate.java
@@ -34,7 +34,7 @@
 
   @Override
   public Instant getMinTimestamp() {
-    return Instant.ofEpochMilli(0);
+    return Instant.EPOCH;
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/query/change/ChangeData.java b/java/com/google/gerrit/server/query/change/ChangeData.java
index 036c0ed..94dad84 100644
--- a/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -96,7 +96,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -276,7 +276,7 @@
             .id(PatchSet.id(id, currentPatchSetId))
             .commitId(commitId)
             .uploader(Account.id(1000))
-            .createdOn(TimeUtil.nowTs())
+            .createdOn(TimeUtil.now())
             .build();
     return cd;
   }
@@ -358,7 +358,7 @@
   private Integer unresolvedCommentCount;
   private Integer totalCommentCount;
   private LabelTypes labelTypes;
-  private Optional<Timestamp> mergedOn;
+  private Optional<Instant> mergedOn;
   private ImmutableSetMultimap<NameKey, RefState> refStates;
   private ImmutableList<byte[]> refStatePatterns;
 
@@ -709,7 +709,7 @@
    * @throws StorageException if {@code lazyLoad} is off, {@link ChangeNotes} can not be loaded
    *     because we do not expect to call the database.
    */
-  public Optional<Timestamp> getMergedOn() throws StorageException {
+  public Optional<Instant> getMergedOn() throws StorageException {
     if (mergedOn == null) {
       // The value was not loaded yet, try to get from the database.
       mergedOn = notes().getMergedOn();
@@ -718,7 +718,7 @@
   }
 
   /** Sets the value e.g. when loading from index. */
-  public void setMergedOn(@Nullable Timestamp mergedOn) {
+  public void setMergedOn(@Nullable Instant mergedOn) {
     this.mergedOn = Optional.ofNullable(mergedOn);
   }
 
@@ -1338,7 +1338,7 @@
 
     public abstract Account.Id author();
 
-    public abstract Timestamp ts();
+    public abstract Instant ts();
   }
 
   @AutoValue
diff --git a/java/com/google/gerrit/server/query/change/ChangePredicates.java b/java/com/google/gerrit/server/query/change/ChangePredicates.java
index 6630cdc..355f9de 100644
--- a/java/com/google/gerrit/server/query/change/ChangePredicates.java
+++ b/java/com/google/gerrit/server/query/change/ChangePredicates.java
@@ -207,6 +207,11 @@
     return new ChangeIndexPredicate(ChangeField.FUZZY_TOPIC, topic);
   }
 
+  /** Returns a predicate that matches changes in the provided {@code topic}. Used with prefixes */
+  public static Predicate<ChangeData> prefixTopic(String topic) {
+    return new ChangeIndexPredicate(ChangeField.PREFIX_TOPIC, topic);
+  }
+
   /** Returns a predicate that matches changes submitted in the provided {@code changeSet}. */
   public static Predicate<ChangeData> submissionId(String changeSet) {
     return new ChangeIndexPredicate(ChangeField.SUBMISSIONID, changeSet);
@@ -231,6 +236,15 @@
         ChangeField.FUZZY_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
   }
 
+  /**
+   * Returns a predicate that matches changes in the provided {@code hashtag}. Used with prefixes
+   */
+  public static Predicate<ChangeData> prefixHashtag(String hashtag) {
+    // Use toLowerCase without locale to match behavior in ChangeField.
+    return new ChangeIndexPredicate(
+        ChangeField.PREFIX_HASHTAG, HashtagsUtil.cleanupHashtag(hashtag).toLowerCase());
+  }
+
   /** Returns a predicate that matches changes that modified the provided {@code file}. */
   public static Predicate<ChangeData> file(ChangeQueryBuilder.Arguments args, String file) {
     Predicate<ChangeData> eqPath = path(file);
diff --git a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 80b3322..28c4da3 100644
--- a/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -870,6 +870,16 @@
   }
 
   @Operator
+  public Predicate<ChangeData> prefixhashtag(String hashtag) throws QueryParseException {
+    if (hashtag.isEmpty()) {
+      return ChangePredicates.hashtag(hashtag);
+    }
+
+    checkFieldAvailable(ChangeField.PREFIX_HASHTAG, "prefixhashtag");
+    return ChangePredicates.prefixHashtag(hashtag);
+  }
+
+  @Operator
   public Predicate<ChangeData> topic(String name) {
     return ChangePredicates.exactTopic(name);
   }
@@ -886,6 +896,16 @@
   }
 
   @Operator
+  public Predicate<ChangeData> prefixtopic(String name) throws QueryParseException {
+    if (name.isEmpty()) {
+      return ChangePredicates.exactTopic(name);
+    }
+
+    checkFieldAvailable(ChangeField.PREFIX_TOPIC, "prefixtopic");
+    return ChangePredicates.prefixTopic(name);
+  }
+
+  @Operator
   public Predicate<ChangeData> ref(String ref) throws QueryParseException {
     if (ref.startsWith("^")) {
       return new RegexRefPredicate(ref);
diff --git a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
index ad3c56b..fbd99eb 100644
--- a/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
+++ b/java/com/google/gerrit/server/restapi/account/DeleteDraftComments.java
@@ -56,7 +56,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -123,7 +123,7 @@
     HumanCommentFormatter humanCommentFormatter =
         commentJsonProvider.get().newHumanCommentFormatter();
     Account.Id accountId = rsrc.getUser().getAccountId();
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     Map<Project.NameKey, BatchUpdate> updates = new LinkedHashMap<>();
     List<Op> ops = new ArrayList<>();
     for (ChangeData cd :
diff --git a/java/com/google/gerrit/server/restapi/account/GetDetail.java b/java/com/google/gerrit/server/restapi/account/GetDetail.java
index 1091599..ba7a37f 100644
--- a/java/com/google/gerrit/server/restapi/account/GetDetail.java
+++ b/java/com/google/gerrit/server/restapi/account/GetDetail.java
@@ -48,7 +48,7 @@
   public Response<AccountDetailInfo> apply(AccountResource rsrc) throws PermissionBackendException {
     Account a = rsrc.getUser().getAccount();
     AccountDetailInfo info = new AccountDetailInfo(a.id().get());
-    info.registeredOn = a.registeredOn();
+    info.setRegisteredOn(a.registeredOn());
     info.inactive = !a.isActive() ? true : null;
     directory.fillAccountInfo(Collections.singleton(info), EnumSet.allOf(FillOptions.class));
     return Response.ok(info);
diff --git a/java/com/google/gerrit/server/restapi/change/Abandon.java b/java/com/google/gerrit/server/restapi/change/Abandon.java
index fa4a887..98514b2 100644
--- a/java/com/google/gerrit/server/restapi/change/Abandon.java
+++ b/java/com/google/gerrit/server/restapi/change/Abandon.java
@@ -126,7 +126,7 @@
     AccountState accountState = user.isIdentifiedUser() ? user.asIdentifiedUser().state() : null;
     AbandonOp op = abandonOpFactory.create(accountState, msgTxt);
     ChangeData changeData = changeDataFactory.create(notes.getProjectName(), notes.getChangeId());
-    try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(notes.getProjectName(), user, TimeUtil.now())) {
       u.setNotify(notify);
       u.addOp(notes.getChangeId(), op);
       u.addOp(
diff --git a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
index cb1256c..a21431e 100644
--- a/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/AddToAttentionSet.java
@@ -92,7 +92,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+            changeResource.getChange().getProject(), changeResource.getUser(), TimeUtil.now())) {
       AddToAttentionSetOp op = opFactory.create(attentionUserId, input.reason, true);
       bu.addOp(changeResource.getId(), op);
       NotifyHandling notify = input.notify == null ? NotifyHandling.OWNER : input.notify;
diff --git a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
index 5375936..0fc5716 100644
--- a/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CherryPickChange.java
@@ -67,7 +67,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -170,7 +170,7 @@
         patch.commitId(),
         input,
         dest,
-        TimeUtil.nowTs(),
+        TimeUtil.now(),
         null,
         null,
         null,
@@ -205,7 +205,7 @@
       throws IOException, InvalidChangeOperationException, UpdateException, RestApiException,
           ConfigInvalidException, NoSuchProjectException {
     return cherryPick(
-        sourceChange, project, sourceCommit, input, dest, TimeUtil.nowTs(), null, null, null, null);
+        sourceChange, project, sourceCommit, input, dest, TimeUtil.now(), null, null, null, null);
   }
 
   /**
@@ -243,7 +243,7 @@
       ObjectId sourceCommit,
       CherryPickInput input,
       BranchNameKey dest,
-      Timestamp timestamp,
+      Instant timestamp,
       @Nullable Change.Id revertedChange,
       @Nullable ObjectId changeIdForNewChange,
       @Nullable Change.Id idForNewChange,
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index fa47bef..6a637b3 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -84,8 +84,9 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collections;
+import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.TimeZone;
@@ -319,6 +320,9 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private ChangeInfo createNewChange(
       ChangeInput input,
       IdentifiedUser me,
@@ -353,13 +357,14 @@
 
       RevCommit mergeTip = parentCommit == null ? null : rw.parseCommit(parentCommit);
 
-      Timestamp now = TimeUtil.nowTs();
+      Instant now = TimeUtil.now();
 
       PersonIdent committer = me.newCommitterIdent(now, serverTimeZone);
       PersonIdent author =
           input.author == null
               ? committer
-              : new PersonIdent(input.author.name, input.author.email, now, serverTimeZone);
+              : new PersonIdent(
+                  input.author.name, input.author.email, Date.from(now), serverTimeZone);
 
       String commitMessage = getCommitMessage(input.subject, me);
 
diff --git a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
index 8476767..9e9cf6a 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateDraftComment.java
@@ -82,8 +82,7 @@
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
 
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       Op op = new Op(rsrc.getPatchSet().id(), in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
index e943e47..651bf7b 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateMergePatchSet.java
@@ -67,7 +67,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
 import java.util.List;
 import java.util.TimeZone;
 import org.eclipse.jgit.lib.ObjectId;
@@ -122,6 +123,9 @@
     this.permissionBackend = permissionBackend;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Override
   public Response<ChangeInfo> apply(ChangeResource rsrc, MergePatchSetInput in)
       throws IOException, RestApiException, UpdateException, PermissionBackendException {
@@ -176,12 +180,12 @@
         currentPsCommit = rw.parseCommit(ps.commitId());
       }
 
-      Timestamp now = TimeUtil.nowTs();
+      Instant now = TimeUtil.now();
       IdentifiedUser me = user.get().asIdentifiedUser();
       PersonIdent author =
           in.author == null
               ? me.newCommitterIdent(now, serverTimeZone)
-              : new PersonIdent(in.author.name, in.author.email, now, serverTimeZone);
+              : new PersonIdent(in.author.name, in.author.email, Date.from(now), serverTimeZone);
       CodeReviewCommit newCommit =
           createMergeCommit(
               in,
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
index d867e00..d818210 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteAssignee.java
@@ -67,8 +67,7 @@
       throws RestApiException, UpdateException, PermissionBackendException {
     rsrc.permissions().check(ChangePermission.EDIT_ASSIGNEE);
 
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       Op op = new Op();
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChange.java b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
index 3ca5463..8298abb 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChange.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChange.java
@@ -54,8 +54,7 @@
     }
     rsrc.permissions().check(ChangePermission.DELETE);
 
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       Change.Id id = rsrc.getChange().getId();
       bu.addOp(id, opFactory.create(id));
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
index 0e868e70..588d56e 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteChangeMessage.java
@@ -89,7 +89,7 @@
     DeleteChangeMessageOp deleteChangeMessageOp =
         new DeleteChangeMessageOp(resource.getChangeMessageId(), newChangeMessage);
     try (BatchUpdate batchUpdate =
-        updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.nowTs())) {
+        updateFactory.create(resource.getChangeResource().getProject(), user, TimeUtil.now())) {
       batchUpdate.addOp(resource.getChangeId(), deleteChangeMessageOp).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteComment.java b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
index 044fd77..2056664 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteComment.java
@@ -84,7 +84,7 @@
     String newMessage = getCommentNewMessage(user.asIdentifiedUser().getName(), input.reason);
     DeleteCommentOp deleteCommentOp = new DeleteCommentOp(rsrc, newMessage);
     try (BatchUpdate batchUpdate =
-        updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getRevisionResource().getProject(), user, TimeUtil.now())) {
       batchUpdate.addOp(rsrc.getRevisionResource().getChange().getId(), deleteCommentOp).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
index 51a0b8e..7d28a39 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteDraftComment.java
@@ -54,7 +54,7 @@
   public Response<CommentInfo> apply(DraftCommentResource rsrc, Input input)
       throws RestApiException, UpdateException {
     try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
       Op op = new Op(rsrc.getComment().key);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
index 16b7136..08725b5 100644
--- a/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/DeletePrivate.java
@@ -62,8 +62,7 @@
     }
 
     SetPrivateOp op = setPrivateOpFactory.create(false, input);
-    try (BatchUpdate u =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       u.addOp(rsrc.getId(), op).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
index db8e9de..7a409e8 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteReviewer.java
@@ -58,7 +58,7 @@
         updateFactory.create(
             rsrc.getChangeResource().getProject(),
             rsrc.getChangeResource().getUser(),
-            TimeUtil.nowTs())) {
+            TimeUtil.now())) {
       bu.setNotify(getNotify(rsrc.getChange(), input));
       BatchUpdateOp op;
       if (rsrc.isByEmail()) {
diff --git a/java/com/google/gerrit/server/restapi/change/DeleteVote.java b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
index b266031..208cecf 100644
--- a/java/com/google/gerrit/server/restapi/change/DeleteVote.java
+++ b/java/com/google/gerrit/server/restapi/change/DeleteVote.java
@@ -134,7 +134,7 @@
 
     try (BatchUpdate bu =
         updateFactory.create(
-            change.getProject(), r.getChangeResource().getUser(), TimeUtil.nowTs())) {
+            change.getProject(), r.getChangeResource().getUser(), TimeUtil.now())) {
       bu.setNotify(
           notifyResolver.resolve(
               firstNonNull(input.notify, NotifyHandling.ALL), input.notifyDetails));
diff --git a/java/com/google/gerrit/server/restapi/change/Move.java b/java/com/google/gerrit/server/restapi/change/Move.java
index 17fe1ce..900b9e5 100644
--- a/java/com/google/gerrit/server/restapi/change/Move.java
+++ b/java/com/google/gerrit/server/restapi/change/Move.java
@@ -159,7 +159,7 @@
     projectCache.get(project).orElseThrow(illegalState(project)).checkStatePermitsWrite();
 
     Op op = new Op(input);
-    try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(project, caller, TimeUtil.now())) {
       u.addOp(change.getId(), op);
       u.execute();
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PostHashtags.java b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
index c1a6a13..bcaa145 100644
--- a/java/com/google/gerrit/server/restapi/change/PostHashtags.java
+++ b/java/com/google/gerrit/server/restapi/change/PostHashtags.java
@@ -48,7 +48,7 @@
     req.permissions().check(ChangePermission.EDIT_HASHTAGS);
 
     try (BatchUpdate bu =
-        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
       SetHashtagsOp op = hashtagsFactory.create(input);
       bu.addOp(req.getId(), op);
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/PostPrivate.java b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
index f774457..45d7250 100644
--- a/java/com/google/gerrit/server/restapi/change/PostPrivate.java
+++ b/java/com/google/gerrit/server/restapi/change/PostPrivate.java
@@ -74,8 +74,7 @@
     }
 
     SetPrivateOp op = setPrivateOpFactory.create(true, input);
-    try (BatchUpdate u =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate u = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       u.addOp(rsrc.getId(), op).execute();
     }
 
diff --git a/java/com/google/gerrit/server/restapi/change/PostReview.java b/java/com/google/gerrit/server/restapi/change/PostReview.java
index 0c64402..d4e6205 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReview.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReview.java
@@ -137,6 +137,7 @@
 import com.google.inject.Singleton;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -274,10 +275,10 @@
   public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException, PatchListNotAvailableException {
-    return apply(revision, input, TimeUtil.nowTs());
+    return apply(revision, input, TimeUtil.now());
   }
 
-  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Timestamp ts)
+  public Response<ReviewResult> apply(RevisionResource revision, ReviewInput input, Instant ts)
       throws RestApiException, UpdateException, IOException, PermissionBackendException,
           ConfigInvalidException, PatchListNotAvailableException {
     // Respect timestamp, but truncate at change created-on time.
@@ -530,7 +531,7 @@
       ChangeData cd,
       PatchSet patchSet,
       List<ReviewerModification> reviewerModifications,
-      Timestamp when) {
+      Instant when) {
     List<AccountState> newlyAddedReviewers = new ArrayList<>();
 
     // There are no events for CCs and reviewers added/deleted by email.
@@ -1203,7 +1204,7 @@
                     parent);
           } else {
             // In ChangeUpdate#putComment() the draft with the same ID will be deleted.
-            comment.writtenOn = ctx.getWhen();
+            comment.writtenOn = Timestamp.from(ctx.getWhen());
             comment.side = inputComment.side();
             comment.message = inputComment.message;
           }
diff --git a/java/com/google/gerrit/server/restapi/change/PostReviewers.java b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
index 4691550..9bc80a4 100644
--- a/java/com/google/gerrit/server/restapi/change/PostReviewers.java
+++ b/java/com/google/gerrit/server/restapi/change/PostReviewers.java
@@ -70,8 +70,7 @@
     if (modification.op == null) {
       return Response.ok(modification.result);
     }
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       bu.setNotify(resolveNotify(rsrc, input));
       Change.Id id = rsrc.getChange().getId();
       bu.addOp(id, modification.op);
diff --git a/java/com/google/gerrit/server/restapi/change/PutAssignee.java b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
index dcf616c..d41620e 100644
--- a/java/com/google/gerrit/server/restapi/change/PutAssignee.java
+++ b/java/com/google/gerrit/server/restapi/change/PutAssignee.java
@@ -98,7 +98,7 @@
     }
 
     try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
       SetAssigneeOp op = assigneeFactory.create(assignee);
       bu.addOp(rsrc.getId(), op);
 
diff --git a/java/com/google/gerrit/server/restapi/change/PutDescription.java b/java/com/google/gerrit/server/restapi/change/PutDescription.java
index 7c54074..5b5bc15 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDescription.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDescription.java
@@ -57,7 +57,7 @@
 
     Op op = new Op(input != null ? input : new DescriptionInput(), rsrc.getPatchSet().id());
     try (BatchUpdate u =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
       u.addOp(rsrc.getChange().getId(), op);
       u.execute();
     }
diff --git a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
index 84a3d89..6411087 100644
--- a/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
+++ b/java/com/google/gerrit/server/restapi/change/PutDraftComment.java
@@ -40,7 +40,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.Optional;
 
@@ -87,7 +87,7 @@
           String.format("Invalid inReplyTo, comment %s not found", in.inReplyTo));
     }
     try (BatchUpdate bu =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
       Op op = new Op(rsrc.getComment().key, in);
       bu.addOp(rsrc.getChange().getId(), op);
       bu.execute();
@@ -145,7 +145,7 @@
     }
   }
 
-  private static HumanComment update(HumanComment e, DraftInput in, Timestamp when) {
+  private static HumanComment update(HumanComment e, DraftInput in, Instant when) {
     if (in.side != null) {
       e.side = in.side();
     }
@@ -154,7 +154,7 @@
     }
     e.setLineNbrAndRange(in.line, in.range);
     e.message = in.message.trim();
-    e.writtenOn = when;
+    e.setWrittenOn(when);
     if (in.tag != null) {
       // TODO(dborowitz): Can we support changing tags via PUT?
       e.tag = in.tag;
diff --git a/java/com/google/gerrit/server/restapi/change/PutMessage.java b/java/com/google/gerrit/server/restapi/change/PutMessage.java
index 1ed7fd7..c62200a 100644
--- a/java/com/google/gerrit/server/restapi/change/PutMessage.java
+++ b/java/com/google/gerrit/server/restapi/change/PutMessage.java
@@ -47,7 +47,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -126,7 +126,7 @@
         throw new ResourceConflictException("new and existing commit message are the same");
       }
 
-      Timestamp ts = TimeUtil.nowTs();
+      Instant ts = TimeUtil.now();
       try (BatchUpdate bu =
           updateFactory.create(resource.getChange().getProject(), userProvider.get(), ts)) {
         // Ensure that BatchUpdate will update the same repo
@@ -161,7 +161,7 @@
       ObjectInserter objectInserter,
       RevCommit basePatchSetCommit,
       String commitMessage,
-      Timestamp timestamp)
+      Instant timestamp)
       throws IOException {
     CommitBuilder builder = new CommitBuilder();
     builder.setTreeId(basePatchSetCommit.getTree());
diff --git a/java/com/google/gerrit/server/restapi/change/PutTopic.java b/java/com/google/gerrit/server/restapi/change/PutTopic.java
index 3031781..c9b436e 100644
--- a/java/com/google/gerrit/server/restapi/change/PutTopic.java
+++ b/java/com/google/gerrit/server/restapi/change/PutTopic.java
@@ -63,7 +63,7 @@
 
     SetTopicOp op = topicOpFactory.create(sanitizedInput.topic);
     try (BatchUpdate u =
-        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(req.getChange().getProject(), req.getUser(), TimeUtil.now())) {
       u.addOp(req.getId(), op);
       u.execute();
     }
diff --git a/java/com/google/gerrit/server/restapi/change/Rebase.java b/java/com/google/gerrit/server/restapi/change/Rebase.java
index 2077fb8..1a0f2b6 100644
--- a/java/com/google/gerrit/server/restapi/change/Rebase.java
+++ b/java/com/google/gerrit/server/restapi/change/Rebase.java
@@ -114,7 +114,7 @@
         ObjectReader reader = oi.newReader();
         RevWalk rw = CodeReviewCommit.newRevWalk(reader);
         BatchUpdate bu =
-            updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+            updateFactory.create(change.getProject(), rsrc.getUser(), TimeUtil.now())) {
       if (!change.isNew()) {
         throw new ResourceConflictException("change is " + ChangeUtil.status(change));
       } else if (!hasOneParent(rw, rsrc.getPatchSet())) {
diff --git a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
index 7fe463e..bd3e8ec 100644
--- a/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
+++ b/java/com/google/gerrit/server/restapi/change/RemoveFromAttentionSet.java
@@ -81,7 +81,7 @@
     ChangeResource changeResource = attentionResource.getChangeResource();
     try (BatchUpdate bu =
         updateFactory.create(
-            changeResource.getProject(), changeResource.getUser(), TimeUtil.nowTs())) {
+            changeResource.getProject(), changeResource.getUser(), TimeUtil.now())) {
       RemoveFromAttentionSetOp op =
           opFactory.create(attentionResource.getAccountId(), input.reason, true);
       bu.addOp(changeResource.getId(), op);
diff --git a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
index 0fabd69..49286fc 100644
--- a/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
+++ b/java/com/google/gerrit/server/restapi/change/ReplyAttentionSetUpdates.java
@@ -151,7 +151,7 @@
             commentsUtil.newHumanComment(
                 changeNotes,
                 currentUser,
-                TimeUtil.nowTs(),
+                TimeUtil.now(),
                 commentInput.path,
                 commentInput.patchSet == null
                     ? changeNotes.getChange().currentPatchSetId()
diff --git a/java/com/google/gerrit/server/restapi/change/Restore.java b/java/com/google/gerrit/server/restapi/change/Restore.java
index b2d1d3a..19d0677 100644
--- a/java/com/google/gerrit/server/restapi/change/Restore.java
+++ b/java/com/google/gerrit/server/restapi/change/Restore.java
@@ -100,7 +100,7 @@
 
     Op op = new Op(input);
     try (BatchUpdate u =
-        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+        updateFactory.create(rsrc.getChange().getProject(), rsrc.getUser(), TimeUtil.now())) {
       u.addOp(rsrc.getId(), op).execute();
     }
     return Response.ok(json.noOptions().format(op.change));
diff --git a/java/com/google/gerrit/server/restapi/change/Revert.java b/java/com/google/gerrit/server/restapi/change/Revert.java
index 8d48c88..7dd3e7a 100644
--- a/java/com/google/gerrit/server/restapi/change/Revert.java
+++ b/java/com/google/gerrit/server/restapi/change/Revert.java
@@ -47,7 +47,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
 @Singleton
@@ -99,12 +98,11 @@
     if (patch == null) {
       throw new ResourceNotFoundException(changeIdToRevert.toString());
     }
-    Timestamp timestamp = TimeUtil.nowTs();
     return Response.ok(
         json.noOptions()
             .format(
                 rsrc.getProject(),
-                commitUtil.createRevertChange(notes, rsrc.getUser(), input, timestamp)));
+                commitUtil.createRevertChange(notes, rsrc.getUser(), input, TimeUtil.now())));
   }
 
   @Override
diff --git a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
index 8bde6e7..383eda0 100644
--- a/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
+++ b/java/com/google/gerrit/server/restapi/change/RevertSubmission.java
@@ -80,8 +80,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -251,7 +251,7 @@
     Multimap<BranchNameKey, ChangeData> changesPerProjectAndBranch = ArrayListMultimap.create();
     changeData.stream().forEach(c -> changesPerProjectAndBranch.put(c.change().getDest(), c));
     cherryPickInput = createCherryPickInput(revertInput);
-    Timestamp timestamp = TimeUtil.nowTs();
+    Instant timestamp = TimeUtil.now();
 
     for (BranchNameKey projectAndBranch : changesPerProjectAndBranch.keySet()) {
       cherryPickInput.base = null;
@@ -290,7 +290,7 @@
       Project.NameKey project,
       Iterator<PatchSetData> sortedChangesInProjectAndBranch,
       Set<ObjectId> commitIdsInProjectAndBranch,
-      Timestamp timestamp)
+      Instant timestamp)
       throws IOException, RestApiException, UpdateException, ConfigInvalidException,
           PermissionBackendException {
 
@@ -314,10 +314,7 @@
   }
 
   private void createCherryPickedRevert(
-      RevertInput revertInput,
-      Project.NameKey project,
-      ChangeNotes changeNotes,
-      Timestamp timestamp)
+      RevertInput revertInput, Project.NameKey project, ChangeNotes changeNotes, Instant timestamp)
       throws IOException, ConfigInvalidException, UpdateException, RestApiException {
     ObjectId revCommitId =
         commitUtil.createRevertCommit(revertInput.message, changeNotes, user.get(), timestamp);
@@ -326,7 +323,7 @@
     cherryPickInput.message = revertInput.message;
     ObjectId generatedChangeId = CommitMessageUtil.generateChangeId();
     Change.Id cherryPickRevertChangeId = Change.id(seq.nextChangeId());
-    try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.setNotify(
           notifyResolver.resolve(
               firstNonNull(cherryPickInput.notify, NotifyHandling.ALL),
@@ -349,7 +346,7 @@
   }
 
   private void craeteNormalRevert(
-      RevertInput revertInput, ChangeNotes changeNotes, Timestamp timestamp)
+      RevertInput revertInput, ChangeNotes changeNotes, Instant timestamp)
       throws IOException, RestApiException, UpdateException, ConfigInvalidException {
 
     Change.Id revertId =
@@ -558,14 +555,14 @@
     private final ObjectId revCommitId;
     private final ObjectId computedChangeId;
     private final Change.Id cherryPickRevertChangeId;
-    private final Timestamp timestamp;
+    private final Instant timestamp;
     private final boolean workInProgress;
 
     CreateCherryPickOp(
         ObjectId revCommitId,
         ObjectId computedChangeId,
         Change.Id cherryPickRevertChangeId,
-        Timestamp timestamp,
+        Instant timestamp,
         Boolean workInProgress) {
       this.revCommitId = revCommitId;
       this.computedChangeId = computedChangeId;
diff --git a/java/com/google/gerrit/server/restapi/change/Revisions.java b/java/com/google/gerrit/server/restapi/change/Revisions.java
index e11ab75..41fecaf 100644
--- a/java/com/google/gerrit/server/restapi/change/Revisions.java
+++ b/java/com/google/gerrit/server/restapi/change/Revisions.java
@@ -39,7 +39,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
@@ -155,6 +154,9 @@
     return ImmutableList.of();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private ImmutableList<RevisionResource> loadEdit(
       ChangeResource change, @Nullable ObjectId commitId) throws AuthException, IOException {
     Optional<ChangeEdit> edit = editUtil.byChange(change.getNotes(), change.getUser());
@@ -165,7 +167,7 @@
               .id(PatchSet.id(change.getId(), 0))
               .commitId(editCommit)
               .uploader(change.getUser().getAccountId())
-              .createdOn(new Timestamp(editCommit.getCommitterIdent().getWhen().getTime()))
+              .createdOn(editCommit.getCommitterIdent().getWhen().toInstant())
               .build();
       if (commitId == null || editCommit.equals(commitId)) {
         return ImmutableList.of(new RevisionResource(change, ps, edit));
diff --git a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
index c118766..9f019b6 100644
--- a/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
+++ b/java/com/google/gerrit/server/restapi/change/SetReadyForReview.java
@@ -63,8 +63,7 @@
       throw new ResourceConflictException("change is not work in progress");
     }
 
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.ALL)));
       bu.addOp(rsrc.getChange().getId(), opFactory.create(false, input));
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
index fdaad9d..0ad5180 100644
--- a/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
+++ b/java/com/google/gerrit/server/restapi/change/SetWorkInProgress.java
@@ -63,8 +63,7 @@
       throw new ResourceConflictException("change is already work in progress");
     }
 
-    try (BatchUpdate bu =
-        updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = updateFactory.create(rsrc.getProject(), rsrc.getUser(), TimeUtil.now())) {
       bu.setNotify(NotifyResolver.Result.create(firstNonNull(input.notify, NotifyHandling.NONE)));
       bu.addOp(rsrc.getChange().getId(), opFactory.create(true, input));
       bu.execute();
diff --git a/java/com/google/gerrit/server/restapi/config/ListTasks.java b/java/com/google/gerrit/server/restapi/config/ListTasks.java
index a12cb5b..dcc44ae 100644
--- a/java/com/google/gerrit/server/restapi/config/ListTasks.java
+++ b/java/com/google/gerrit/server/restapi/config/ListTasks.java
@@ -36,7 +36,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import java.time.Instant;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -119,7 +119,7 @@
   public static class TaskInfo {
     public String id;
     public Task.State state;
-    public Instant startTime;
+    public Timestamp startTime;
     public long delay;
     public String command;
     public String remoteName;
@@ -129,7 +129,7 @@
     public TaskInfo(Task<?> task) {
       this.id = HexFormat.fromInt(task.getTaskId());
       this.state = task.getState();
-      this.startTime = task.getStartTime();
+      this.startTime = Timestamp.from(task.getStartTime());
       this.delay = task.getDelay(TimeUnit.MILLISECONDS);
       this.command = task.toString();
       this.queueName = task.getQueueName();
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index ee86010..f257f86 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -212,7 +212,7 @@
             createGroupArgs.uuid,
             GroupUuid.make(
                 createGroupArgs.getGroupName(),
-                self.get().newCommitterIdent(TimeUtil.nowTs(), serverTimeZone)));
+                self.get().newCommitterIdent(TimeUtil.now(), serverTimeZone)));
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
             .setGroupUUID(uuid)
diff --git a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
index e3aa0f3..1b0fcd4 100644
--- a/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
+++ b/java/com/google/gerrit/server/restapi/group/GetAuditLog.java
@@ -102,7 +102,7 @@
           auditEvents.add(
               GroupAuditEventInfo.createRemoveUserEvent(
                   accountLoader.get(auditEvent.removedBy().orElse(null)),
-                  auditEvent.removedOn(),
+                  auditEvent.removedOn().orElse(null),
                   member));
         }
       }
@@ -134,7 +134,7 @@
           auditEvents.add(
               GroupAuditEventInfo.createRemoveGroupEvent(
                   accountLoader.get(auditEvent.removedBy().orElse(null)),
-                  auditEvent.removedOn(),
+                  auditEvent.removedOn().orElse(null),
                   member));
         }
       }
diff --git a/java/com/google/gerrit/server/restapi/group/GroupJson.java b/java/com/google/gerrit/server/restapi/group/GroupJson.java
index e1459c3..6d3fa01 100644
--- a/java/com/google/gerrit/server/restapi/group/GroupJson.java
+++ b/java/com/google/gerrit/server/restapi/group/GroupJson.java
@@ -121,7 +121,7 @@
       }
     }
 
-    info.createdOn = internalGroup.getCreatedOn();
+    info.setCreatedOn(internalGroup.getCreatedOn());
 
     if (options.contains(MEMBERS)) {
       info.members = listMembers.get().getDirectMembers(internalGroup, groupControlSupplier.get());
diff --git a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
index 75dd014..8a0cc39 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateAccessChange.java
@@ -152,7 +152,7 @@
           ObjectReader objReader = objInserter.newReader();
           RevWalk rw = new RevWalk(objReader);
           BatchUpdate bu =
-              updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.nowTs())) {
+              updateFactory.create(rsrc.getNameKey(), rsrc.getUser(), TimeUtil.now())) {
         bu.setRepository(md.getRepository(), rw, objInserter);
         ChangeInserter ins = newInserter(changeId, commit);
         bu.insertChange(ins);
diff --git a/java/com/google/gerrit/server/restapi/project/CreateTag.java b/java/com/google/gerrit/server/restapi/project/CreateTag.java
index b552ff5..6980006 100644
--- a/java/com/google/gerrit/server/restapi/project/CreateTag.java
+++ b/java/com/google/gerrit/server/restapi/project/CreateTag.java
@@ -136,7 +136,7 @@
                   resource
                       .getUser()
                       .asIdentifiedUser()
-                      .newCommitterIdent(TimeUtil.nowTs(), TimeZone.getDefault()));
+                      .newCommitterIdent(TimeUtil.now(), TimeZone.getDefault()));
         }
 
         Ref result = tag.call();
diff --git a/java/com/google/gerrit/server/restapi/project/GetReflog.java b/java/com/google/gerrit/server/restapi/project/GetReflog.java
index d36ca22..e0131ee 100644
--- a/java/com/google/gerrit/server/restapi/project/GetReflog.java
+++ b/java/com/google/gerrit/server/restapi/project/GetReflog.java
@@ -23,7 +23,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.extensions.restapi.RestReadView;
 import com.google.gerrit.server.CommonConverters;
-import com.google.gerrit.server.args4j.TimestampHandler;
+import com.google.gerrit.server.args4j.InstantHandler;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.permissions.PermissionBackend;
 import com.google.gerrit.server.permissions.PermissionBackendException;
@@ -31,7 +31,7 @@
 import com.google.gerrit.server.project.BranchResource;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
 import org.eclipse.jgit.lib.ReflogEntry;
@@ -60,9 +60,9 @@
       metaVar = "TIMESTAMP",
       usage =
           "timestamp from which the reflog entries should be listed (UTC, format: "
-              + TimestampHandler.TIMESTAMP_FORMAT
+              + InstantHandler.TIMESTAMP_FORMAT
               + ")")
-  public GetReflog setFrom(Timestamp from) {
+  public GetReflog setFrom(Instant from) {
     this.from = from;
     return this;
   }
@@ -72,16 +72,16 @@
       metaVar = "TIMESTAMP",
       usage =
           "timestamp until which the reflog entries should be listed (UTC, format: "
-              + TimestampHandler.TIMESTAMP_FORMAT
+              + InstantHandler.TIMESTAMP_FORMAT
               + ")")
-  public GetReflog setTo(Timestamp to) {
+  public GetReflog setTo(Instant to) {
     this.to = to;
     return this;
   }
 
   private int limit;
-  private Timestamp from;
-  private Timestamp to;
+  private Instant from;
+  private Instant to;
 
   @Inject
   public GetReflog(GitRepositoryManager repoManager, PermissionBackend permissionBackend) {
@@ -89,6 +89,9 @@
     this.permissionBackend = permissionBackend;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Override
   public Response<List<ReflogEntryInfo>> apply(BranchResource rsrc)
       throws RestApiException, IOException, PermissionBackendException {
@@ -115,8 +118,8 @@
       } else {
         entries = limit > 0 ? new ArrayList<>(limit) : new ArrayList<>();
         for (ReflogEntry e : r.getReverseEntries()) {
-          Timestamp timestamp = new Timestamp(e.getWho().getWhen().getTime());
-          if ((from == null || from.before(timestamp)) && (to == null || to.after(timestamp))) {
+          Instant timestamp = e.getWho().getWhen().toInstant();
+          if ((from == null || from.isBefore(timestamp)) && (to == null || to.isAfter(timestamp))) {
             entries.add(e);
           }
           if (limit > 0 && entries.size() >= limit) {
diff --git a/java/com/google/gerrit/server/restapi/project/ListTags.java b/java/com/google/gerrit/server/restapi/project/ListTags.java
index 123c78a..eccdcfc 100644
--- a/java/com/google/gerrit/server/restapi/project/ListTags.java
+++ b/java/com/google/gerrit/server/restapi/project/ListTags.java
@@ -39,7 +39,7 @@
 import com.google.gerrit.server.project.RefFilter;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -172,6 +172,9 @@
     throw new ResourceNotFoundException(id);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public static TagInfo createTagInfo(
       PermissionBackend.ForRef perm, Ref ref, RevWalk rw, ProjectState projectState, WebLinks links)
       throws IOException {
@@ -197,12 +200,12 @@
           tagger != null ? CommonConverters.toGitPerson(tagger) : null,
           canDelete,
           webLinks.isEmpty() ? null : webLinks,
-          tagger != null ? new Timestamp(tagger.getWhen().getTime()) : null);
+          tagger != null ? tagger.getWhen().toInstant() : null);
     }
 
-    Timestamp timestamp =
+    Instant timestamp =
         object instanceof RevCommit
-            ? new Timestamp(((RevCommit) object).getCommitterIdent().getWhen().getTime())
+            ? ((RevCommit) object).getCommitterIdent().getWhen().toInstant()
             : null;
 
     // Lightweight tag
diff --git a/java/com/google/gerrit/server/submit/CherryPick.java b/java/com/google/gerrit/server/submit/CherryPick.java
index 71e248f..84b0ab7 100644
--- a/java/com/google/gerrit/server/submit/CherryPick.java
+++ b/java/com/google/gerrit/server/submit/CherryPick.java
@@ -102,8 +102,7 @@
       args.rw.parseBody(mergeTip);
       String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
 
-      PersonIdent committer =
-          args.caller.newCommitterIdent(ctx.getWhen(), args.serverIdent.getTimeZone());
+      PersonIdent committer = ctx.newCommitterIdent(args.caller);
       try {
         newCommit =
             args.mergeUtil.createCherryPickFromCommit(
@@ -196,7 +195,7 @@
           && !args.subscriptionGraph.hasSubscription(args.destBranch)) {
         mergeTip.moveTipTo(toMerge, toMerge);
       } else {
-        PersonIdent myIdent = new PersonIdent(args.serverIdent, ctx.getWhen());
+        PersonIdent myIdent = ctx.newPersonIdent(args.serverIdent);
         CodeReviewCommit result =
             args.mergeUtil.mergeOneCommit(
                 myIdent,
diff --git a/java/com/google/gerrit/server/submit/MergeOneOp.java b/java/com/google/gerrit/server/submit/MergeOneOp.java
index f1b93e1..1840479 100644
--- a/java/com/google/gerrit/server/submit/MergeOneOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOneOp.java
@@ -29,9 +29,7 @@
 
   @Override
   public void updateRepoImpl(RepoContext ctx) throws IntegrationConflictException, IOException {
-    PersonIdent caller =
-        ctx.getIdentifiedUser()
-            .newCommitterIdent(args.serverIdent.getWhen(), args.serverIdent.getTimeZone());
+    PersonIdent caller = ctx.getIdentifiedUser().newCommitterIdent(args.serverIdent);
     if (args.mergeTip.getCurrentTip() == null) {
       throw new IllegalStateException(
           "cannot merge commit "
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index 238e6ea..0160fc9 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -43,7 +43,7 @@
 import com.google.gerrit.entities.SubmitRequirement;
 import com.google.gerrit.entities.SubmitRequirementResult;
 import com.google.gerrit.entities.SubmitTypeRecord;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
 import com.google.gerrit.exceptions.StorageException;
 import com.google.gerrit.extensions.api.changes.NotifyHandling;
 import com.google.gerrit.extensions.api.changes.SubmitInput;
@@ -89,7 +89,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -248,7 +248,7 @@
   // Changes that were updated by this MergeOp.
   private final Map<Change.Id, Change> updatedChanges;
 
-  private Timestamp ts;
+  private Instant ts;
   private SubmissionId submissionId;
   private IdentifiedUser caller;
 
@@ -444,7 +444,7 @@
             firstNonNull(submitInput.notify, NotifyHandling.ALL), submitInput.notifyDetails);
     this.dryrun = dryrun;
     this.caller = caller;
-    this.ts = TimeUtil.nowTs();
+    this.ts = TimeUtil.now();
     this.submissionId = new SubmissionId(change);
 
     try (TraceContext traceContext =
@@ -514,7 +514,7 @@
                   boolean isRetry = attempt > 1;
                   if (isRetry) {
                     logger.atFine().log("Retrying, attempt #%d; skipping merged changes", attempt);
-                    this.ts = TimeUtil.nowTs();
+                    this.ts = TimeUtil.now();
                     openRepoManager();
                   }
                   this.commitStatus = new CommitStatus(filteredNoteDbChangeSet, isRetry);
@@ -690,7 +690,7 @@
       if (e.getCause() instanceof IntegrationConflictException) {
         throw (IntegrationConflictException) e.getCause();
       }
-      throw new InternalServerWithUserMessageException(genericMergeError(cs), e);
+      throw new MergeUpdateException(genericMergeError(cs), e);
     }
   }
 
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index 8981b07..2024448 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -37,7 +37,7 @@
 import com.google.gerrit.server.update.BatchUpdate;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -167,7 +167,7 @@
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
 
-  private Timestamp ts;
+  private Instant ts;
   private IdentifiedUser caller;
   private NotifyResolver.Result notify;
 
@@ -185,7 +185,7 @@
     openRepos = new HashMap<>();
   }
 
-  public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
+  public void setContext(Instant ts, IdentifiedUser caller, NotifyResolver.Result notify) {
     this.ts = requireNonNull(ts);
     this.caller = requireNonNull(caller);
     this.notify = requireNonNull(notify);
diff --git a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
index 8aef3c7..1409775 100644
--- a/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
+++ b/java/com/google/gerrit/server/submit/RebaseSubmitStrategy.java
@@ -166,8 +166,7 @@
         RevCommit mergeTip = args.mergeTip.getCurrentTip();
         args.rw.parseBody(mergeTip);
         String cherryPickCmtMsg = args.mergeUtil.createCommitMessageOnSubmit(toMerge, mergeTip);
-        PersonIdent committer =
-            args.caller.newCommitterIdent(ctx.getWhen(), args.serverIdent.getTimeZone());
+        PersonIdent committer = ctx.newCommitterIdent(args.caller);
         try {
           newCommit =
               args.mergeUtil.createCherryPickFromCommit(
@@ -304,8 +303,7 @@
           && !args.subscriptionGraph.hasSubscription(args.destBranch)) {
         mergeTip.moveTipTo(toMerge, toMerge);
       } else {
-        PersonIdent caller =
-            ctx.getIdentifiedUser().newCommitterIdent(ctx.getWhen(), ctx.getTimeZone());
+        PersonIdent caller = ctx.newCommitterIdent();
         CodeReviewCommit newTip =
             args.mergeUtil.mergeOneCommit(
                 caller,
diff --git a/java/com/google/gerrit/server/update/BatchUpdate.java b/java/com/google/gerrit/server/update/BatchUpdate.java
index 46bf094..9edfdc4 100644
--- a/java/com/google/gerrit/server/update/BatchUpdate.java
+++ b/java/com/google/gerrit/server/update/BatchUpdate.java
@@ -68,7 +68,7 @@
 import com.google.inject.Module;
 import com.google.inject.assistedinject.Assisted;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
@@ -122,7 +122,7 @@
   }
 
   public interface Factory {
-    BatchUpdate create(Project.NameKey project, CurrentUser user, Timestamp when);
+    BatchUpdate create(Project.NameKey project, CurrentUser user, Instant when);
   }
 
   public static void execute(
@@ -252,7 +252,7 @@
     }
 
     @Override
-    public Timestamp getWhen() {
+    public Instant getWhen() {
       return when;
     }
 
@@ -376,7 +376,7 @@
 
   private final Project.NameKey project;
   private final CurrentUser user;
-  private final Timestamp when;
+  private final Instant when;
   private final TimeZone tz;
 
   private final ListMultimap<Change.Id, BatchUpdateOp> ops =
@@ -405,7 +405,7 @@
       GitReferenceUpdated gitRefUpdated,
       @Assisted Project.NameKey project,
       @Assisted CurrentUser user,
-      @Assisted Timestamp when) {
+      @Assisted Instant when) {
     this.repoManager = repoManager;
     this.changeDataFactory = changeDataFactory;
     this.changeNotesFactory = changeNotesFactory;
diff --git a/java/com/google/gerrit/server/update/Context.java b/java/com/google/gerrit/server/update/Context.java
index 9947168..57ebedd 100644
--- a/java/com/google/gerrit/server/update/Context.java
+++ b/java/com/google/gerrit/server/update/Context.java
@@ -24,8 +24,9 @@
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.change.NotifyResolver;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.TimeZone;
+import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.revwalk.RevWalk;
 
 /**
@@ -66,7 +67,7 @@
    *
    * @return timestamp.
    */
-  Timestamp getWhen();
+  Instant getWhen();
 
   /**
    * Get the time zone in which this update takes place.
@@ -134,4 +135,33 @@
   default Account.Id getAccountId() {
     return getIdentifiedUser().getAccountId();
   }
+
+  /**
+   * Creates a new {@link PersonIdent} with {@link #getWhen()} as timestamp.
+   *
+   * @param personIdent {@link PersonIdent} to be copied
+   * @return copied {@link PersonIdent} with {@link #getWhen()} as timestamp
+   */
+  default PersonIdent newPersonIdent(PersonIdent personIdent) {
+    return new PersonIdent(personIdent, getWhen().toEpochMilli(), personIdent.getTimeZoneOffset());
+  }
+
+  /**
+   * Creates a committer {@link PersonIdent} for {@link #getIdentifiedUser()}.
+   *
+   * @return the created committer {@link PersonIdent}
+   */
+  default PersonIdent newCommitterIdent() {
+    return newCommitterIdent(getIdentifiedUser());
+  }
+
+  /**
+   * Creates a committer {@link PersonIdent} for the given user.
+   *
+   * @param user user for which a committer {@link PersonIdent} should be created
+   * @return the created committer {@link PersonIdent}
+   */
+  default PersonIdent newCommitterIdent(IdentifiedUser user) {
+    return user.newCommitterIdent(getWhen(), getTimeZone());
+  }
 }
diff --git a/java/com/google/gerrit/server/util/AttentionSetUtil.java b/java/com/google/gerrit/server/util/AttentionSetUtil.java
index 98200fd..26c8f47 100644
--- a/java/com/google/gerrit/server/util/AttentionSetUtil.java
+++ b/java/com/google/gerrit/server/util/AttentionSetUtil.java
@@ -28,7 +28,6 @@
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.notedb.ChangeNotes;
 import java.io.IOException;
-import java.sql.Timestamp;
 import java.util.Collection;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 
@@ -123,7 +122,7 @@
             : null;
     return new AttentionSetInfo(
         accountLoader.get(attentionSetUpdate.account()),
-        Timestamp.from(attentionSetUpdate.timestamp()),
+        attentionSetUpdate.timestamp(),
         attentionSetUpdate.reason(),
         reasonAccount);
   }
diff --git a/java/com/google/gerrit/server/util/time/TimeUtil.java b/java/com/google/gerrit/server/util/time/TimeUtil.java
index 54ef305..f89324b 100644
--- a/java/com/google/gerrit/server/util/time/TimeUtil.java
+++ b/java/com/google/gerrit/server/util/time/TimeUtil.java
@@ -15,10 +15,7 @@
 package com.google.gerrit.server.util.time;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.gerrit.common.UsedAt;
-import com.google.gerrit.common.UsedAt.Project;
 import com.google.gerrit.server.util.git.DelegateSystemReader;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.concurrent.TimeUnit;
 import java.util.function.LongSupplier;
@@ -44,23 +41,8 @@
     return Instant.ofEpochMilli(nowMs());
   }
 
-  public static Timestamp nowTs() {
-    return new Timestamp(nowMs());
-  }
-
-  /**
-   * Returns the magic timestamp representing no specific time.
-   *
-   * <p>This "null object" is helpful in contexts where using {@code null} directly is not possible.
-   */
-  @UsedAt(Project.PLUGIN_CHECKS)
-  public static Timestamp never() {
-    // Always create a new object as timestamps are mutable.
-    return new Timestamp(0);
-  }
-
-  public static Timestamp truncateToSecond(Timestamp t) {
-    return new Timestamp((t.getTime() / 1000) * 1000);
+  public static Instant truncateToSecond(Instant t) {
+    return Instant.ofEpochMilli(t.getEpochSecond() * 1000);
   }
 
   @VisibleForTesting
diff --git a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
index 35cb3ba..244fdbe 100644
--- a/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
+++ b/java/com/google/gerrit/sshd/commands/SetTopicCommand.java
@@ -84,8 +84,7 @@
 
     for (ChangeResource r : changes.values()) {
       SetTopicOp op = topicOpFactory.create(topic);
-      try (BatchUpdate u =
-          updateFactory.create(r.getChange().getProject(), user, TimeUtil.nowTs())) {
+      try (BatchUpdate u = updateFactory.create(r.getChange().getProject(), user, TimeUtil.now())) {
         u.addOp(r.getId(), op);
         u.execute();
       }
diff --git a/java/com/google/gerrit/sshd/commands/ShowCaches.java b/java/com/google/gerrit/sshd/commands/ShowCaches.java
index 62bdb32..5b89228 100644
--- a/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -43,8 +43,9 @@
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
 import java.io.IOException;
-import java.text.SimpleDateFormat;
 import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.Collection;
 import java.util.Map;
 import org.apache.sshd.common.io.IoAcceptor;
@@ -115,11 +116,13 @@
     enableGracefulStop();
     nw = columns - 50;
     Instant now = Instant.now();
+    DateTimeFormatter fmt =
+        DateTimeFormatter.ofPattern("HH:mm:ss   zzz").withZone(ZoneId.of("UTC"));
     stdout.format(
         "%-25s %-20s      now  %16s\n",
         "Gerrit Code Review",
         Version.getVersion() != null ? Version.getVersion() : "",
-        new SimpleDateFormat("HH:mm:ss   zzz").format(now));
+        fmt.format(now));
     stdout.format(
         "%-25s %-20s   uptime %16s\n", "", "", uptime(now.toEpochMilli() - serverStarted));
     stdout.print('\n');
diff --git a/java/com/google/gerrit/sshd/commands/ShowQueue.java b/java/com/google/gerrit/sshd/commands/ShowQueue.java
index 928cdda..4254e5b 100644
--- a/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -155,7 +155,7 @@
         stdout.print(
             String.format(
                 "%8s %-12s %-12s %-4s %s\n",
-                task.id, start, startTime(task.startTime), "", command));
+                task.id, start, startTime(task.startTime.toInstant()), "", command));
       } else {
         String remoteName =
             task.remoteName != null ? task.remoteName + "/" + task.projectName : task.projectName;
@@ -165,7 +165,7 @@
                 "%8s %-12s %-4s %s\n",
                 task.id,
                 start,
-                startTime(task.startTime),
+                startTime(task.startTime.toInstant()),
                 MoreObjects.firstNonNull(remoteName, "n/a")));
       }
     }
diff --git a/java/com/google/gerrit/testing/FakeAccountCache.java b/java/com/google/gerrit/testing/FakeAccountCache.java
index ab3348b..49a8d71 100644
--- a/java/com/google/gerrit/testing/FakeAccountCache.java
+++ b/java/com/google/gerrit/testing/FakeAccountCache.java
@@ -40,7 +40,7 @@
       return state;
     }
     return newState(
-        Account.builder(accountId, TimeUtil.nowTs())
+        Account.builder(accountId, TimeUtil.now())
             .setMetaId("1234567812345678123456781234567812345678")
             .build());
   }
diff --git a/java/com/google/gerrit/testing/IndexVersions.java b/java/com/google/gerrit/testing/IndexVersions.java
index 3281ffc..f245665 100644
--- a/java/com/google/gerrit/testing/IndexVersions.java
+++ b/java/com/google/gerrit/testing/IndexVersions.java
@@ -27,7 +27,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
-import java.util.SortedMap;
+import java.util.NavigableMap;
 import org.eclipse.jgit.lib.Config;
 
 public class IndexVersions {
@@ -90,7 +90,7 @@
       value = value.trim();
     }
 
-    SortedMap<Integer, Schema<V>> schemas = schemaDef.getSchemas();
+    NavigableMap<Integer, Schema<V>> schemas = schemaDef.getSchemas();
     if (!Strings.isNullOrEmpty(value)) {
       if (ALL.equals(value)) {
         return ImmutableList.copyOf(schemas.keySet());
diff --git a/java/com/google/gerrit/testing/TestChanges.java b/java/com/google/gerrit/testing/TestChanges.java
index b795c5b..8bd02b8 100644
--- a/java/com/google/gerrit/testing/TestChanges.java
+++ b/java/com/google/gerrit/testing/TestChanges.java
@@ -58,7 +58,7 @@
             changeId,
             userId,
             BranchNameKey.create(project, "master"),
-            TimeUtil.nowTs());
+            TimeUtil.now());
     incrementPatchSet(c);
     return c;
   }
@@ -72,7 +72,7 @@
         .id(id)
         .commitId(ObjectId.fromString(revision))
         .uploader(userId)
-        .createdOn(TimeUtil.nowTs())
+        .createdOn(TimeUtil.now())
         .build();
   }
 
@@ -94,7 +94,7 @@
                         injector.getInstance(AbstractChangeNotes.Args.class), c, shouldExist, null)
                     .load(),
                 user,
-                TimeUtil.nowTs(),
+                TimeUtil.now(),
                 Ordering.natural());
 
     ChangeNotes notes = update.getNotes();
diff --git a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
index 7d04558..877ccd5 100644
--- a/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
+++ b/javatests/com/google/gerrit/acceptance/ProjectResetterTest.java
@@ -36,6 +36,7 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import com.google.gerrit.testing.TestTimeUtil;
 import java.io.IOException;
+import java.util.Date;
 import java.util.concurrent.TimeUnit;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
@@ -332,10 +333,13 @@
     assertThat(repo.exactRef(ref.getName())).isNull();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private ObjectId createCommit(Repository repo) throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter()) {
       PersonIdent ident =
-          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
       cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
index ecfe3f5..9d689ba 100644
--- a/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
+++ b/javatests/com/google/gerrit/acceptance/annotation/UseClockStepTest.java
@@ -19,7 +19,6 @@
 import com.google.gerrit.acceptance.AbstractDaemonTest;
 import com.google.gerrit.acceptance.UseClockStep;
 import com.google.gerrit.server.util.time.TimeUtil;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.concurrent.TimeUnit;
 import org.junit.Test;
@@ -52,6 +51,6 @@
   @Test
   @UseClockStep(startAtEpoch = true)
   public void useClockStepWithStartAtEpoch() {
-    assertThat(TimeUtil.nowTs()).isEqualTo(Timestamp.from(Instant.EPOCH));
+    assertThat(TimeUtil.now()).isEqualTo(Instant.EPOCH);
   }
 }
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
index 915e759..3678e25 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountIT.java
@@ -158,6 +158,7 @@
 import java.security.KeyPair;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -499,7 +500,7 @@
       assertThat(ref).isNotNull();
       RevCommit c = rw.parseCommit(ref.getObjectId());
       long timestampDiffMs =
-          Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).registeredOn().getTime());
+          Math.abs(c.getCommitTime() * 1000L - getAccount(accountId).registeredOn().toEpochMilli());
       assertThat(timestampDiffMs).isAtMost(SECONDS.toMillis(1));
 
       // Check the 'account.config' file.
@@ -980,7 +981,8 @@
     assertThat(detail.email).isEqualTo(email);
     assertThat(detail.secondaryEmails).containsExactly(secondaryEmail);
     assertThat(detail.status).isEqualTo(status);
-    assertThat(detail.registeredOn).isEqualTo(getAccount(foo.id()).registeredOn());
+    assertThat(detail.registeredOn.getTime())
+        .isEqualTo(getAccount(foo.id()).registeredOn().toEpochMilli());
     assertThat(detail.inactive).isNull();
     assertThat(detail._moreAccounts).isNull();
   }
@@ -2464,6 +2466,9 @@
   }
 
   @Test
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public void stalenessChecker() throws Exception {
     // Newly created account is not stale.
     AccountInfo accountInfo = gApi.accounts().create(name("foo")).get();
@@ -2477,7 +2482,7 @@
         RevWalk rw = new RevWalk(repo)) {
       RevCommit commit = rw.parseCommit(repo.exactRef(userRef).getObjectId());
 
-      PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+      PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(commit.getTree());
       cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index 38e465f..66dbe80 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -211,6 +211,7 @@
 import java.io.IOException;
 import java.sql.Timestamp;
 import java.text.MessageFormat;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -1936,7 +1937,7 @@
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
-    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+    Instant oldTs = rsrc.getChange().getLastUpdatedOn();
 
     addReviewer.call(r.getChangeId(), user.email());
 
@@ -2047,7 +2048,7 @@
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
-    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+    Instant oldTs = rsrc.getChange().getLastUpdatedOn();
 
     // create a group named "ab" with one user: testUser
     String email = "abcd@example.com";
@@ -2096,7 +2097,7 @@
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
-    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+    Instant oldTs = rsrc.getChange().getLastUpdatedOn();
 
     // create a group named "kobe" with one user: lee
     String testUserFullname = "kobebryant";
@@ -2197,7 +2198,7 @@
     PushOneCommit.Result r = createChange();
     ChangeResource rsrc = parseResource(r);
     String oldETag = rsrc.getETag();
-    Timestamp oldTs = rsrc.getChange().getLastUpdatedOn();
+    Instant oldTs = rsrc.getChange().getLastUpdatedOn();
 
     ReviewerInput in = new ReviewerInput();
     in.reviewer = user.email();
@@ -3411,7 +3412,7 @@
       assertThat(commitPatchSetCreation.getShortMessage()).isEqualTo("Create patch set 2");
       PersonIdent expectedAuthor =
           changeNoteUtil.newAccountIdIdent(
-              getAccount(admin.id()).id(), c.updated, serverIdent.get());
+              getAccount(admin.id()).id(), c.updated.toInstant(), serverIdent.get());
       assertThat(commitPatchSetCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitPatchSetCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.updated));
@@ -3421,7 +3422,7 @@
       assertThat(commitChangeCreation.getShortMessage()).isEqualTo("Create change");
       expectedAuthor =
           changeNoteUtil.newAccountIdIdent(
-              getAccount(admin.id()).id(), c.created, serverIdent.get());
+              getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
       assertThat(commitChangeCreation.getAuthorIdent()).isEqualTo(expectedAuthor);
       assertThat(commitChangeCreation.getCommitterIdent())
           .isEqualTo(new PersonIdent(serverIdent.get(), c.created));
@@ -6660,7 +6661,7 @@
 
   private void setChangeStatus(Change.Id id, Change.Status newStatus) throws Exception {
     try (BatchUpdate batchUpdate =
-        batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.nowTs())) {
+        batchUpdateFactory.create(project, atrScope.get().getUser(), TimeUtil.now())) {
       batchUpdate.addOp(id, new ChangeStatusUpdateOp(newStatus));
       batchUpdate.execute();
     }
diff --git a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
index 97b7148..267f5a7 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/PrivateChangeIT.java
@@ -251,7 +251,7 @@
   private void markMergedChangePrivate(Change.Id changeId) throws Exception {
     try (BatchUpdate u =
         batchUpdateFactory.create(
-            project, identifiedUserFactory.create(admin.id()), TimeUtil.nowTs())) {
+            project, identifiedUserFactory.create(admin.id()), TimeUtil.now())) {
       u.addOp(
               changeId,
               new BatchUpdateOp() {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java b/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
index 079d43e9..27f6111 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupAssert.java
@@ -42,7 +42,7 @@
     assertThat(toBoolean(info.options.visibleToAll)).isEqualTo(group.isVisibleToAll());
     assertThat(info.description).isEqualTo(group.getDescription());
     assertThat(Url.decode(info.ownerId)).isEqualTo(group.getOwnerGroupUUID().get());
-    assertThat(info.createdOn).isEqualTo(group.getCreatedOn());
+    assertThat(info.createdOn.toInstant()).isEqualTo(group.getCreatedOn());
   }
 
   public static boolean toBoolean(Boolean b) {
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 0c806b7..63b67f8 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -109,8 +109,10 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.Target;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Stream;
@@ -557,14 +559,17 @@
     assertThrows(AuthException.class, () -> gApi.groups().create(name("newGroup")));
   }
 
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void createdOnFieldIsPopulatedForNewGroup() throws Exception {
     // NoteDb allows only second precision.
-    Timestamp testStartTime = TimeUtil.truncateToSecond(TimeUtil.nowTs());
+    Instant testStartTime = TimeUtil.truncateToSecond(TimeUtil.now());
     String newGroupName = name("newGroup");
     GroupInfo group = gApi.groups().create(newGroupName).get();
 
-    assertThat(group.createdOn).isAtLeast(testStartTime);
+    assertThat(group.createdOn.toInstant()).isAtLeast(testStartTime);
   }
 
   @Test
@@ -1603,6 +1608,9 @@
     return createCommit(repo, commitMessage, null);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private ObjectId createCommit(Repository repo, String commitMessage, @Nullable ObjectId treeId)
       throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter()) {
@@ -1610,7 +1618,7 @@
         treeId = oi.insert(Constants.OBJ_TREE, new byte[] {});
       }
 
-      PersonIdent ident = new PersonIdent(serverIdent.get(), TimeUtil.nowTs());
+      PersonIdent ident = new PersonIdent(serverIdent.get(), Date.from(TimeUtil.now()));
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(treeId);
       cb.setCommitter(ident);
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
index 656451d..dcd274d 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionDiffIT.java
@@ -57,7 +57,7 @@
 import java.awt.image.BufferedImage;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.text.SimpleDateFormat;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -2969,6 +2969,9 @@
     return "An unchanged patchset\n\nChange-Id: " + changeId;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private void assertDiffForNewFile(
       PushOneCommit.Result pushResult, String path, String expectedContentSideB) throws Exception {
     DiffInfo diff =
@@ -2987,16 +2990,18 @@
           abbreviateName(parentCommit, 8, testRepo.getRevWalk().getObjectReader());
       headers.add("Parent:     " + parentCommitId + " (" + parentCommit.getShortMessage() + ")");
 
-      SimpleDateFormat dtfmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z", Locale.US);
       PersonIdent author = c.getAuthorIdent();
-      dtfmt.setTimeZone(author.getTimeZone());
+      DateTimeFormatter fmt =
+          DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss Z")
+              .withLocale(Locale.US)
+              .withZone(author.getTimeZone().toZoneId());
       headers.add("Author:     " + author.getName() + " <" + author.getEmailAddress() + ">");
-      headers.add("AuthorDate: " + dtfmt.format(author.getWhen().getTime()));
+      headers.add("AuthorDate: " + fmt.format(author.getWhen().toInstant()));
 
       PersonIdent committer = c.getCommitterIdent();
-      dtfmt.setTimeZone(committer.getTimeZone());
+      fmt = fmt.withZone(committer.getTimeZone().toZoneId());
       headers.add("Commit:     " + committer.getName() + " <" + committer.getEmailAddress() + ">");
-      headers.add("CommitDate: " + dtfmt.format(committer.getWhen().getTime()));
+      headers.add("CommitDate: " + fmt.format(committer.getWhen().toInstant()));
       headers.add("");
     }
 
diff --git a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
index d3fe83f..72b5f93 100644
--- a/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/revision/RevisionIT.java
@@ -100,7 +100,6 @@
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.inject.Inject;
 import java.io.ByteArrayOutputStream;
-import java.sql.Timestamp;
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
 import java.util.Collection;
@@ -1683,10 +1682,15 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  // TODO(issue-15508): Migrate timestamp fields in *Info/*Input classes from type Timestamp to
+  // Instant
+  @SuppressWarnings("JdkObsolete")
   private void assertPersonIdent(GitPerson gitPerson, PersonIdent expectedIdent) {
     assertThat(gitPerson.name).isEqualTo(expectedIdent.getName());
     assertThat(gitPerson.email).isEqualTo(expectedIdent.getEmailAddress());
-    assertThat(gitPerson.date).isEqualTo(new Timestamp(expectedIdent.getWhen().getTime()));
+    assertThat(gitPerson.date.getTime()).isEqualTo(expectedIdent.getWhen().getTime());
     assertThat(gitPerson.tz).isEqualTo(expectedIdent.getTimeZoneOffset());
   }
 
diff --git a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
index a1e9bf1..c89e11a 100644
--- a/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/account/GetAccountDetailIT.java
@@ -59,7 +59,7 @@
     AccountDetailInfo info = newGson().fromJson(r.getReader(), AccountDetailInfo.class);
     assertAccountInfo(admin, info);
     Account account = getAccount(admin.id());
-    assertThat(info.registeredOn).isEqualTo(account.registeredOn());
+    assertThat(info.registeredOn.getTime()).isEqualTo(account.registeredOn().toEpochMilli());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
index b55561b..de14d00 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AbstractSubmit.java
@@ -1126,7 +1126,7 @@
   private void setChangeStatusToNew(PushOneCommit.Result... changes) throws Throwable {
     for (PushOneCommit.Result change : changes) {
       try (BatchUpdate bu =
-          batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.nowTs())) {
+          batchUpdateFactory.create(project, userFactory.create(admin.id()), TimeUtil.now())) {
         bu.addOp(
             change.getChange().getId(),
             new BatchUpdateOp() {
@@ -1249,8 +1249,8 @@
     submit(r.getChangeId());
     assertThat(r.getChange().getMergedOn()).isPresent();
     ChangeInfo change = gApi.changes().id(r.getChangeId()).get();
-    assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.updated);
-    assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.submitted);
+    assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.getUpdated());
+    assertThat(r.getChange().getMergedOn().get()).isEqualTo(change.getSubmitted());
   }
 
   @Override
@@ -1360,6 +1360,9 @@
     assertThat(actual.getTimeZone()).isEqualTo(expected.getTimeZone());
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   protected void assertAuthorAndCommitDateEquals(RevCommit commit) {
     assertThat(commit.getAuthorIdent().getWhen().getTime())
         .isEqualTo(commit.getCommitterIdent().getWhen().getTime());
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
index 9246442..b034a42 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/AttentionSetIT.java
@@ -2049,7 +2049,7 @@
     comment.side = Side.REVISION;
     comment.path = Patch.COMMIT_MSG;
     comment.message = "comment";
-    comment.updated = TimeUtil.nowTs();
+    comment.setUpdated(TimeUtil.now());
     comment.inReplyTo = id;
     ReviewInput reviewInput = new ReviewInput();
     reviewInput.comments = ImmutableMap.of(Patch.COMMIT_MSG, ImmutableList.of(comment));
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 186e73e..dbebbf9 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -594,7 +594,7 @@
 
       PersonIdent expectedAuthor =
           changeNoteUtil.newAccountIdIdent(
-              getAccount(admin.id()).id(), c.created, serverIdent.get());
+              getAccount(admin.id()).id(), c.created.toInstant(), serverIdent.get());
       assertThat(commit.getAuthorIdent()).isEqualTo(expectedAuthor);
 
       assertThat(commit.getCommitterIdent())
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
index aa93815..2eade27 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/SubmitByRebaseAlwaysIT.java
@@ -33,7 +33,7 @@
 import com.google.gerrit.entities.PatchSet;
 import com.google.gerrit.entities.PatchSetApproval;
 import com.google.gerrit.entities.Project.NameKey;
-import com.google.gerrit.exceptions.InternalServerWithUserMessageException;
+import com.google.gerrit.exceptions.MergeUpdateException;
 import com.google.gerrit.extensions.api.changes.ReviewInput;
 import com.google.gerrit.extensions.client.InheritableBoolean;
 import com.google.gerrit.extensions.client.SubmitType;
@@ -136,8 +136,8 @@
     ChangeMessageModifier modifier2 = (msg, orig, tip, dest) -> msg + "A-footer: value\n";
     try (Registration registration =
         extensionRegistry.newRegistration().add(modifier1).add(modifier2)) {
-      InternalServerWithUserMessageException thrown =
-          assertThrows(InternalServerWithUserMessageException.class, () -> submitWithRebase());
+      MergeUpdateException thrown =
+          assertThrows(MergeUpdateException.class, () -> submitWithRebase());
       Throwable cause = Throwables.getRootCause(thrown);
       assertThat(cause).isInstanceOf(RuntimeException.class);
       assertThat(cause).hasMessageThat().isEqualTo("boom");
@@ -153,8 +153,8 @@
             .newRegistration()
             .add(modifier1, "modifier-1")
             .add(modifier2, "modifier-2")) {
-      InternalServerWithUserMessageException thrown =
-          assertThrows(InternalServerWithUserMessageException.class, () -> submitWithRebase());
+      MergeUpdateException thrown =
+          assertThrows(MergeUpdateException.class, () -> submitWithRebase());
       Throwable cause = Throwables.getRootCause(thrown);
       assertThat(cause).isInstanceOf(RuntimeException.class);
       assertThat(cause)
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
index b1879f6..8bf70f7 100644
--- a/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/project/TagsIT.java
@@ -41,7 +41,7 @@
 import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 import com.google.gerrit.server.project.ProjectConfig;
 import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.List;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -222,14 +222,14 @@
     assertThat(result.ref).isEqualTo(R_TAGS + input.ref);
     assertThat(result.revision).isEqualTo(input.revision);
     assertThat(result.canDelete).isTrue();
-    assertThat(result.created).isEqualTo(timestamp(r));
+    assertThat(result.created.toInstant()).isEqualTo(instant(r));
 
     input.ref = "refs/tags/v2.0";
     result = tag(input.ref).create(input).get();
     assertThat(result.ref).isEqualTo(input.ref);
     assertThat(result.revision).isEqualTo(input.revision);
     assertThat(result.canDelete).isTrue();
-    assertThat(result.created).isEqualTo(timestamp(r));
+    assertThat(result.created.toInstant()).isEqualTo(instant(r));
 
     requestScopeOperations.setApiUser(user.id());
     result = tag(input.ref).get();
@@ -457,8 +457,11 @@
     return gApi.projects().name(project.get()).tag(tagname);
   }
 
-  private Timestamp timestamp(PushOneCommit.Result r) {
-    return new Timestamp(r.getCommit().getCommitterIdent().getWhen().getTime());
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  private Instant instant(PushOneCommit.Result r) {
+    return r.getCommit().getCommitterIdent().getWhen().toInstant();
   }
 
   private void assertBadRequest(ListRefsRequest<TagInfo> req) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java b/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java
index ed6800e..e1b4ccb 100644
--- a/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java
+++ b/javatests/com/google/gerrit/acceptance/server/approval/PatchSetApprovalUuidTest.java
@@ -10,7 +10,7 @@
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGenerator;
 import com.google.gerrit.server.approval.PatchSetApprovalUuidGeneratorImpl;
 import com.google.gerrit.server.util.time.TimeUtil;
-import java.util.Date;
+import java.time.Instant;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -30,7 +30,7 @@
       PatchSet.Id patchSetId = PatchSet.id(Change.id(1), 1);
       Account.Id accountId = Account.id(1);
       String label = LabelId.CODE_REVIEW;
-      Date granted = TimeUtil.nowTs();
+      Instant granted = TimeUtil.now();
       PatchSetApproval.UUID uuid1 =
           patchSetApprovalUuidGenerator.get(patchSetId, accountId, label, value, granted);
       PatchSetApproval.UUID uuid2 =
diff --git a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
index 8cdac85..6d980c7 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/CommentsIT.java
@@ -69,7 +69,7 @@
 import com.google.gerrit.testing.FakeEmailSender.Message;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -667,7 +667,7 @@
   public void putDraft() throws Exception {
     for (Integer line : lines) {
       PushOneCommit.Result r = createChange();
-      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+      Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       String path = "file1";
@@ -914,7 +914,7 @@
   public void deleteDraft() throws Exception {
     for (Integer line : lines) {
       PushOneCommit.Result r = createChange();
-      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+      Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
       DraftInput draft = CommentsUtil.newDraft("file1", Side.REVISION, line, "comment 1");
@@ -930,7 +930,7 @@
 
   @Test
   public void insertCommentsWithHistoricTimestamp() throws Exception {
-    Timestamp timestamp = new Timestamp(0);
+    Instant timestamp = Instant.EPOCH;
     for (Integer line : lines) {
       String file = "file";
       String contents = "contents " + line;
@@ -939,11 +939,11 @@
       PushOneCommit.Result r = push.to("refs/for/master");
       String changeId = r.getChangeId();
       String revId = r.getCommit().getName();
-      Timestamp origLastUpdated = r.getChange().change().getLastUpdatedOn();
+      Instant origLastUpdated = r.getChange().change().getLastUpdatedOn();
 
       ReviewInput input = new ReviewInput();
       CommentInput comment = CommentsUtil.newComment(file, Side.REVISION, line, "comment 1", false);
-      comment.updated = timestamp;
+      comment.setUpdated(timestamp);
       input.comments = new HashMap<>();
       input.comments.put(comment.path, Lists.newArrayList(comment));
       ChangeResource changeRsrc =
diff --git a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
index 9d821b7..5b6da36 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/ConsistencyCheckerIT.java
@@ -732,7 +732,7 @@
   }
 
   private BatchUpdate newUpdate(Account.Id owner) {
-    return batchUpdateFactory.create(project, userFactory.create(owner), TimeUtil.nowTs());
+    return batchUpdateFactory.create(project, userFactory.create(owner), TimeUtil.now());
   }
 
   private ChangeNotes insertChange() throws Exception {
@@ -825,10 +825,14 @@
     assertThat(ru.delete()).isEqualTo(RefUpdate.Result.FORCED);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private void addNoteDbCommit(Change.Id id, String commitMessage) throws Exception {
     PersonIdent committer = serverIdent.get();
     PersonIdent author =
-        noteUtil.newAccountIdIdent(getAccount(admin.id()).id(), committer.getWhen(), committer);
+        noteUtil.newAccountIdIdent(
+            getAccount(admin.id()).id(), committer.getWhen().toInstant(), committer);
     serverSideTestRepo
         .branch(RefNames.changeMetaRef(id))
         .commit()
diff --git a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
index e778a5c..fd3ac7f 100644
--- a/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/change/GetRelatedIT.java
@@ -666,7 +666,7 @@
   }
 
   private void clearGroups(PatchSet.Id psId) throws Exception {
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user(user), TimeUtil.now())) {
       bu.addOp(
           psId.changeId(),
           new BatchUpdateOp() {
diff --git a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
index d6fcccc..4e490a7 100644
--- a/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/mail/MailMetadataIT.java
@@ -30,11 +30,10 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.testing.FakeEmailSender;
 import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.util.Collection;
-import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -100,7 +99,7 @@
     expectedHeaders.put("Gerrit-MessageType", "comment");
     expectedHeaders.put("Gerrit-Commit", newChange.getCommit().getId().name());
     expectedHeaders.put("Gerrit-ChangeURL", changeURL);
-    expectedHeaders.put("Gerrit-Comment-Date", Iterables.getLast(result).date);
+    expectedHeaders.put("Gerrit-Comment-Date", Iterables.getLast(result).date.toInstant());
 
     assertHeaders(message.headers(), expectedHeaders);
 
@@ -116,14 +115,14 @@
       if (entry.getValue() instanceof String) {
         assertThat(have)
             .containsEntry("X-" + entry.getKey(), new StringEmailHeader((String) entry.getValue()));
-      } else if (entry.getValue() instanceof Date) {
+      } else if (entry.getValue() instanceof Instant) {
         assertThat(have)
-            .containsEntry("X-" + entry.getKey(), new EmailHeader.Date((Date) entry.getValue()));
+            .containsEntry("X-" + entry.getKey(), new EmailHeader.Date((Instant) entry.getValue()));
       } else {
         throw new Exception(
             "Object has unsupported type: "
                 + entry.getValue().getClass().getName()
-                + " must be java.util.Date or java.lang.String for key "
+                + " must be java.time.Instant or java.lang.String for key "
                 + entry.getKey());
       }
     }
@@ -133,19 +132,18 @@
     for (Map.Entry<String, Object> entry : want.entrySet()) {
       if (entry.getValue() instanceof String) {
         assertThat(body).contains(entry.getKey() + ": " + entry.getValue());
-      } else if (entry.getValue() instanceof Timestamp) {
+      } else if (entry.getValue() instanceof Instant) {
         assertThat(body)
             .contains(
                 entry.getKey()
                     + ": "
                     + MailProcessingUtil.rfcDateformatter.format(
-                        ZonedDateTime.ofInstant(
-                            ((Timestamp) entry.getValue()).toInstant(), ZoneId.of("UTC"))));
+                        ZonedDateTime.ofInstant((Instant) entry.getValue(), ZoneId.of("UTC"))));
       } else {
         throw new Exception(
             "Object has unsupported type: "
                 + entry.getValue().getClass().getName()
-                + " must be java.util.Date or java.lang.String for key "
+                + " must be java.time.Instant or java.lang.String for key "
                 + entry.getKey());
       }
     }
diff --git a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
index 3c066a3..ab5e1d8 100644
--- a/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/notedb/NoteDbOnlyIT.java
@@ -274,7 +274,7 @@
   }
 
   private BatchUpdate newBatchUpdate(BatchUpdate.Factory buf) {
-    return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.nowTs());
+    return buf.create(project, identifiedUserFactory.create(user.id()), TimeUtil.now());
   }
 
   private Optional<ObjectId> getRef(String name) throws Exception {
diff --git a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
index 97f072e..925c855 100644
--- a/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
+++ b/javatests/com/google/gerrit/acceptance/server/query/ApprovalQueryIT.java
@@ -35,7 +35,7 @@
 import com.google.gerrit.server.query.approval.ApprovalContext;
 import com.google.gerrit.server.query.approval.ApprovalQueryBuilder;
 import com.google.inject.Inject;
-import java.util.Date;
+import java.time.Instant;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.junit.Test;
@@ -261,7 +261,7 @@
     PatchSetApproval approval =
         PatchSetApproval.builder()
             .postSubmit(false)
-            .granted(new Date())
+            .granted(Instant.now())
             .key(PatchSetApproval.key(psId, approver, LabelId.create("Code-Review")))
             .value(value)
             .build();
diff --git a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
index a003f9d..473b128 100644
--- a/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
+++ b/javatests/com/google/gerrit/acceptance/testsuite/group/GroupOperationsImplTest.java
@@ -31,7 +31,7 @@
 import com.google.gerrit.extensions.restapi.RestApiException;
 import com.google.gerrit.truth.NullAwareCorrespondence;
 import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 import org.junit.Test;
 
@@ -325,9 +325,9 @@
     GroupInfo group = gApi.groups().create(createArbitraryGroupInput()).detail();
     AccountGroup.UUID groupUuid = AccountGroup.uuid(group.id);
 
-    Timestamp createdOn = groupOperations.group(groupUuid).get().createdOn();
+    Instant createdOn = groupOperations.group(groupUuid).get().createdOn();
 
-    assertThat(createdOn).isEqualTo(group.createdOn);
+    assertThat(createdOn).isEqualTo(group.createdOn.toInstant());
   }
 
   @Test
diff --git a/javatests/com/google/gerrit/entities/LabelFunctionTest.java b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
index 0132697..80d97db 100644
--- a/javatests/com/google/gerrit/entities/LabelFunctionTest.java
+++ b/javatests/com/google/gerrit/entities/LabelFunctionTest.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.ImmutableList;
 import java.time.Instant;
 import java.util.ArrayList;
-import java.util.Date;
 import java.util.List;
 import org.junit.Test;
 
@@ -94,7 +93,7 @@
     return PatchSetApproval.builder()
         .key(PatchSetApproval.key(PS_ID, Account.id(10000 + value), LABEL_ID))
         .value(value)
-        .granted(Date.from(Instant.now()))
+        .granted(Instant.now())
         .build();
   }
 
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
index ec6c372..22daf5b 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeMessageProtoConverterTest.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.util.AccountTemplateUtil;
 import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.junit.Test;
 
 public class ChangeMessageProtoConverterTest {
@@ -40,7 +40,7 @@
         ChangeMessage.create(
             ChangeMessage.key(Change.id(543), "change-message-21"),
             Account.id(63),
-            new Timestamp(9876543),
+            Instant.ofEpochMilli(9876543),
             PatchSet.id(Change.id(34), 13),
             "This is a change message.",
             Account.id(10003),
@@ -73,7 +73,7 @@
         ChangeMessage.create(
             ChangeMessage.key(Change.id(543), "change-message-21"),
             Account.id(63),
-            new Timestamp(9876543),
+            Instant.ofEpochMilli(9876543),
             PatchSet.id(Change.id(34), 13));
 
     Entities.ChangeMessage proto = changeMessageProtoConverter.toProto(changeMessage);
@@ -140,7 +140,7 @@
         ChangeMessage.create(
             ChangeMessage.key(Change.id(543), "change-message-21"),
             Account.id(63),
-            new Timestamp(9876543),
+            Instant.ofEpochMilli(9876543),
             PatchSet.id(Change.id(34), 13),
             "This is a change message.",
             Account.id(10003),
@@ -157,7 +157,7 @@
         ChangeMessage.create(
             ChangeMessage.key(Change.id(543), "change-message-21"),
             Account.id(63),
-            new Timestamp(9876543),
+            Instant.ofEpochMilli(9876543),
             PatchSet.id(Change.id(34), 13),
             String.format(
                 "This is a change message by %s and includes %s ",
@@ -178,7 +178,7 @@
         ChangeMessage.create(
             ChangeMessage.key(Change.id(543), "change-message-21"),
             Account.id(63),
-            new Timestamp(9876543),
+            Instant.ofEpochMilli(9876543),
             PatchSet.id(Change.id(34), 13));
 
     ChangeMessage convertedChangeMessage =
@@ -205,7 +205,7 @@
             ImmutableMap.<String, Type>builder()
                 .put("key", ChangeMessage.Key.class)
                 .put("author", Account.Id.class)
-                .put("writtenOn", Timestamp.class)
+                .put("writtenOn", Instant.class)
                 .put("message", String.class)
                 .put("patchset", PatchSet.Id.class)
                 .put("tag", String.class)
diff --git a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
index 8c5e449..bd4b2b1 100644
--- a/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/ChangeProtoConverterTest.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.proto.Entities;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.junit.Test;
 
 public class ChangeProtoConverterTest {
@@ -41,8 +41,8 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch 74"),
-            new Timestamp(987654L));
-    change.setLastUpdatedOn(new Timestamp(1234567L));
+            Instant.ofEpochMilli(987654L));
+    change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
     change.setStatus(Change.Status.MERGED);
     change.setCurrentPatchSet(
         PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
@@ -90,7 +90,7 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
-            new Timestamp(987654L));
+            Instant.ofEpochMilli(987654L));
 
     Entities.Change proto = changeProtoConverter.toProto(change);
 
@@ -125,7 +125,7 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
-            new Timestamp(987654L));
+            Instant.ofEpochMilli(987654L));
     // O as ID actually means that no current patch set is present.
     change.setCurrentPatchSet(PatchSet.id(Change.id(14), 0), null, null);
 
@@ -162,7 +162,7 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
-            new Timestamp(987654L));
+            Instant.ofEpochMilli(987654L));
     change.setCurrentPatchSet(PatchSet.id(Change.id(14), 23), "subject ABC", null);
 
     Entities.Change proto = changeProtoConverter.toProto(change);
@@ -198,8 +198,8 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
-            new Timestamp(987654L));
-    change.setLastUpdatedOn(new Timestamp(1234567L));
+            Instant.ofEpochMilli(987654L));
+    change.setLastUpdatedOn(Instant.ofEpochMilli(1234567L));
     change.setStatus(Change.Status.MERGED);
     change.setCurrentPatchSet(
         PatchSet.id(Change.id(14), 23), "subject XYZ", "original subject ABC");
@@ -223,7 +223,7 @@
             Change.id(14),
             Account.id(35),
             BranchNameKey.create(Project.nameKey("project 67"), "branch-74"),
-            new Timestamp(987654L));
+            Instant.ofEpochMilli(987654L));
 
     Change convertedChange = changeProtoConverter.fromProto(changeProtoConverter.toProto(change));
     assertEqualChange(convertedChange, change);
@@ -242,8 +242,8 @@
     assertThat(change.getKey()).isNull();
     assertThat(change.getOwner()).isNull();
     assertThat(change.getDest()).isNull();
-    assertThat(change.getCreatedOn()).isEqualTo(new Timestamp(0));
-    assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(0));
+    assertThat(change.getCreatedOn()).isEqualTo(Instant.EPOCH);
+    assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.EPOCH);
     assertThat(change.getSubject()).isNull();
     assertThat(change.currentPatchSetId()).isNull();
     // Default values for unset protobuf fields which can't be unset in the entity object.
@@ -268,7 +268,7 @@
             .build();
     Change change = changeProtoConverter.fromProto(proto);
 
-    assertThat(change.getLastUpdatedOn()).isEqualTo(new Timestamp(987654L));
+    assertThat(change.getLastUpdatedOn()).isEqualTo(Instant.ofEpochMilli(987654L));
   }
 
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
@@ -279,8 +279,8 @@
             ImmutableMap.<String, Type>builder()
                 .put("changeId", Change.Id.class)
                 .put("changeKey", Change.Key.class)
-                .put("createdOn", Timestamp.class)
-                .put("lastUpdatedOn", Timestamp.class)
+                .put("createdOn", Instant.class)
+                .put("lastUpdatedOn", Instant.class)
                 .put("owner", Account.Id.class)
                 .put("dest", BranchNameKey.class)
                 .put("status", char.class)
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
index 0338959..28f9cdb 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetApprovalProtoConverterTest.java
@@ -28,8 +28,7 @@
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.Type;
-import java.sql.Timestamp;
-import java.util.Date;
+import java.time.Instant;
 import java.util.Optional;
 import org.junit.Test;
 
@@ -46,7 +45,7 @@
                     PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
             .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
             .value(456)
-            .granted(new Date(987654L))
+            .granted(Instant.ofEpochMilli(987654L))
             .tag("tag-21")
             .realAccountId(Account.id(612))
             .postSubmit(true)
@@ -84,7 +83,7 @@
                 PatchSetApproval.key(
                     PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
             .value(456)
-            .granted(new Date(987654L))
+            .granted(Instant.ofEpochMilli(987654L))
             .build();
 
     Entities.PatchSetApproval proto = protoConverter.toProto(patchSetApproval);
@@ -117,7 +116,7 @@
                     PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
             .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
             .value(456)
-            .granted(new Date(987654L))
+            .granted(Instant.ofEpochMilli(987654L))
             .tag("tag-21")
             .realAccountId(Account.id(612))
             .postSubmit(true)
@@ -137,7 +136,7 @@
                 PatchSetApproval.key(
                     PatchSet.id(Change.id(42), 14), Account.id(100013), LabelId.create("label-8")))
             .value(456)
-            .granted(new Date(987654L))
+            .granted(Instant.ofEpochMilli(987654L))
             .build();
 
     PatchSetApproval convertedPatchSetApproval =
@@ -167,7 +166,7 @@
     assertThat(patchSetApproval.labelId()).isEqualTo(LabelId.create("label-8"));
     // Default values for unset protobuf fields which can't be unset in the entity object.
     assertThat(patchSetApproval.value()).isEqualTo(0);
-    assertThat(patchSetApproval.granted()).isEqualTo(new Timestamp(0));
+    assertThat(patchSetApproval.granted()).isEqualTo(Instant.EPOCH);
     assertThat(patchSetApproval.postSubmit()).isEqualTo(false);
     assertThat(patchSetApproval.copied()).isEqualTo(false);
   }
@@ -181,7 +180,7 @@
                 .put("key", PatchSetApproval.Key.class)
                 .put("uuid", new TypeLiteral<Optional<PatchSetApproval.UUID>>() {}.getType())
                 .put("value", short.class)
-                .put("granted", Timestamp.class)
+                .put("granted", Instant.class)
                 .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
diff --git a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
index efeb24f..3a534e9 100644
--- a/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
+++ b/javatests/com/google/gerrit/entities/converter/PatchSetProtoConverterTest.java
@@ -27,7 +27,7 @@
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.inject.TypeLiteral;
 import java.lang.reflect.Type;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
 import org.junit.Test;
@@ -42,7 +42,7 @@
             .id(PatchSet.id(Change.id(103), 73))
             .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
             .uploader(Account.id(452))
-            .createdOn(new Timestamp(930349320L))
+            .createdOn(Instant.ofEpochMilli(930349320L))
             .groups(ImmutableList.of("group1", " group2"))
             .pushCertificate("my push certificate")
             .description("This is a patch set description.")
@@ -74,7 +74,7 @@
             .id(PatchSet.id(Change.id(103), 73))
             .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
             .uploader(Account.id(452))
-            .createdOn(new Timestamp(930349320L))
+            .createdOn(Instant.ofEpochMilli(930349320L))
             .build();
 
     Entities.PatchSet proto = patchSetProtoConverter.toProto(patchSet);
@@ -100,7 +100,7 @@
             .id(PatchSet.id(Change.id(103), 73))
             .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
             .uploader(Account.id(452))
-            .createdOn(new Timestamp(930349320L))
+            .createdOn(Instant.ofEpochMilli(930349320L))
             .groups(ImmutableList.of("group1", " group2"))
             .pushCertificate("my push certificate")
             .description("This is a patch set description.")
@@ -118,7 +118,7 @@
             .id(PatchSet.id(Change.id(103), 73))
             .commitId(ObjectId.fromString("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
             .uploader(Account.id(452))
-            .createdOn(new Timestamp(930349320L))
+            .createdOn(Instant.ofEpochMilli(930349320L))
             .build();
 
     PatchSet convertedPatchSet =
@@ -143,7 +143,7 @@
                 .id(PatchSet.id(Change.id(103), 73))
                 .commitId(ObjectId.fromString("0000000000000000000000000000000000000000"))
                 .uploader(Account.id(0))
-                .createdOn(new Timestamp(0))
+                .createdOn(Instant.EPOCH)
                 .build());
   }
 
@@ -156,7 +156,7 @@
                 .put("id", PatchSet.Id.class)
                 .put("commitId", ObjectId.class)
                 .put("uploader", Account.Id.class)
-                .put("createdOn", Timestamp.class)
+                .put("createdOn", Instant.class)
                 .put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
                 .put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("description", new TypeLiteral<Optional<String>>() {}.getType())
diff --git a/javatests/com/google/gerrit/git/BUILD b/javatests/com/google/gerrit/git/BUILD
index c2c9cce..b12924a 100644
--- a/javatests/com/google/gerrit/git/BUILD
+++ b/javatests/com/google/gerrit/git/BUILD
@@ -7,7 +7,10 @@
     size = "medium",
     timeout = "short",
     srcs = MEDIUM_TESTS,
-    tags = ["no_windows"],
+    tags = [
+        "no_rbe",
+        "no_windows",
+    ],
     deps = [
         "//java/com/google/gerrit/git",
         "//lib:guava",
diff --git a/javatests/com/google/gerrit/integration/git/BUILD b/javatests/com/google/gerrit/integration/git/BUILD
index 28755af..bcb16f4 100644
--- a/javatests/com/google/gerrit/integration/git/BUILD
+++ b/javatests/com/google/gerrit/integration/git/BUILD
@@ -3,7 +3,10 @@
 acceptance_tests(
     srcs = ["GitProtocolV2IT.java"],
     group = "protocol-v2",
-    labels = ["git-protocol-v2"],
+    labels = [
+        "git-protocol-v2",
+        "no_rbe",
+    ],
 )
 
 acceptance_tests(
diff --git a/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
index 2699c3b..44ef822 100644
--- a/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
+++ b/javatests/com/google/gerrit/json/SqlTimestampDeserializerTest.java
@@ -16,9 +16,9 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.JsonPrimitive;
 import java.sql.Timestamp;
+import java.time.Instant;
 import org.junit.Test;
 
 public class SqlTimestampDeserializerTest {
@@ -28,6 +28,6 @@
   @Test
   public void emptyStringIsDeserializedToMagicTimestamp() {
     Timestamp timestamp = deserializer.deserialize(new JsonPrimitive(""), Timestamp.class, null);
-    assertThat(timestamp).isEqualTo(TimeUtil.never());
+    assertThat(timestamp).isEqualTo(Timestamp.from(Instant.EPOCH));
   }
 }
diff --git a/javatests/com/google/gerrit/mail/AbstractParserTest.java b/javatests/com/google/gerrit/mail/AbstractParserTest.java
index a2432a2..f99a2af 100644
--- a/javatests/com/google/gerrit/mail/AbstractParserTest.java
+++ b/javatests/com/google/gerrit/mail/AbstractParserTest.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.entities.Address;
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.List;
@@ -58,7 +57,7 @@
         new HumanComment(
             new Comment.Key(uuid, file, 1),
             Account.id(0),
-            new Timestamp(0L),
+            Instant.EPOCH,
             (short) 0,
             message,
             "",
@@ -73,7 +72,7 @@
         new HumanComment(
             new Comment.Key(uuid, file, 1),
             Account.id(0),
-            new Timestamp(0L),
+            Instant.EPOCH,
             (short) 0,
             message,
             "",
diff --git a/javatests/com/google/gerrit/pgm/BUILD b/javatests/com/google/gerrit/pgm/BUILD
index 0fe4fad..0655bb2 100644
--- a/javatests/com/google/gerrit/pgm/BUILD
+++ b/javatests/com/google/gerrit/pgm/BUILD
@@ -4,6 +4,7 @@
 junit_tests(
     name = "pgm_tests",
     srcs = glob(["**/*.java"]),
+    tags = ["no_rbe"],
     deps = [
         "//java/com/google/gerrit/pgm/http/jetty",
         "//java/com/google/gerrit/pgm/init/api",
diff --git a/javatests/com/google/gerrit/server/BUILD b/javatests/com/google/gerrit/server/BUILD
index c694a87..87d1729 100644
--- a/javatests/com/google/gerrit/server/BUILD
+++ b/javatests/com/google/gerrit/server/BUILD
@@ -27,7 +27,10 @@
     ),
     resource_strip_prefix = "resources",
     resources = ["//resources/com/google/gerrit/server"],
-    tags = ["no_windows"],
+    tags = [
+        "no_rbe",
+        "no_windows",
+    ],
     visibility = ["//visibility:public"],
     runtime_deps = [
         "//java/com/google/gerrit/lucene",
diff --git a/javatests/com/google/gerrit/server/IdentifiedUserTest.java b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
index 463af35..855a0bc 100644
--- a/javatests/com/google/gerrit/server/IdentifiedUserTest.java
+++ b/javatests/com/google/gerrit/server/IdentifiedUserTest.java
@@ -99,7 +99,7 @@
     injector.injectMembers(this);
 
     Account account =
-        Account.builder(Account.id(1), TimeUtil.nowTs())
+        Account.builder(Account.id(1), TimeUtil.now())
             .setMetaId("1234567812345678123456781234567812345678")
             .build();
     Account.Id ownerId = account.id();
diff --git a/javatests/com/google/gerrit/server/account/AccountCacheTest.java b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
index a2aa40b..c1eff15 100644
--- a/javatests/com/google/gerrit/server/account/AccountCacheTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountCacheTest.java
@@ -23,7 +23,6 @@
 import com.google.gerrit.entities.Project;
 import com.google.gerrit.server.cache.proto.Cache;
 import com.google.gerrit.server.config.CachedPreferences;
-import java.sql.Timestamp;
 import java.time.Instant;
 import org.junit.Test;
 
@@ -32,8 +31,7 @@
  * of the {@code AccountCache}.
  */
 public class AccountCacheTest {
-  private static final Account ACCOUNT =
-      Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH)).build();
+  private static final Account ACCOUNT = Account.builder(Account.id(1), Instant.EPOCH).build();
   private static final Cache.AccountProto ACCOUNT_PROTO =
       Cache.AccountProto.newBuilder().setId(1).setRegisteredOn(0).build();
   private static final CachedAccountDetails.Serializer SERIALIZER =
@@ -42,7 +40,7 @@
   @Test
   public void account_roundTrip() throws Exception {
     Account account =
-        Account.builder(Account.id(1), Timestamp.from(Instant.EPOCH))
+        Account.builder(Account.id(1), Instant.EPOCH)
             .setFullName("foo bar")
             .setDisplayName("foo")
             .setActive(false)
diff --git a/javatests/com/google/gerrit/server/account/AccountResolverTest.java b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
index 146090c..3658834 100644
--- a/javatests/com/google/gerrit/server/account/AccountResolverTest.java
+++ b/javatests/com/google/gerrit/server/account/AccountResolverTest.java
@@ -361,13 +361,13 @@
 
   private AccountState newAccount(int id) {
     return AccountState.forAccount(
-        Account.builder(Account.id(id), TimeUtil.nowTs())
+        Account.builder(Account.id(id), TimeUtil.now())
             .setMetaId("1234567812345678123456781234567812345678")
             .build());
   }
 
   private AccountState newInactiveAccount(int id) {
-    Account.Builder a = Account.builder(Account.id(id), TimeUtil.nowTs());
+    Account.Builder a = Account.builder(Account.id(id), TimeUtil.now());
     a.setActive(false);
     return AccountState.forAccount(a.build());
   }
diff --git a/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java b/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
index 8d301e4..78947a2 100644
--- a/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
+++ b/javatests/com/google/gerrit/server/cache/serialize/entities/InternalGroupSerializerTest.java
@@ -34,7 +34,7 @@
           .setOwnerGroupUUID(AccountGroup.uuid("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"))
           .setVisibleToAll(false)
           .setGroupUUID(AccountGroup.uuid("deadbeefdeadbeefdeadbeefdeadbeef12345678"))
-          .setCreatedOn(TimeUtil.nowTs())
+          .setCreatedOn(TimeUtil.now())
           .setMembers(ImmutableSet.of(Account.id(123), Account.id(321)))
           .setSubgroups(
               ImmutableSet.of(
diff --git a/javatests/com/google/gerrit/server/change/CommentThreadTest.java b/javatests/com/google/gerrit/server/change/CommentThreadTest.java
index 08485a4..d0b6c14 100644
--- a/javatests/com/google/gerrit/server/change/CommentThreadTest.java
+++ b/javatests/com/google/gerrit/server/change/CommentThreadTest.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.junit.Test;
 
 public class CommentThreadTest {
@@ -60,7 +60,7 @@
     return new HumanComment(
         new Comment.Key(commentUuid, "myFile", 1),
         Account.id(100),
-        new Timestamp(1234),
+        Instant.ofEpochMilli(1234),
         (short) 1,
         "Comment text",
         "serverId",
diff --git a/javatests/com/google/gerrit/server/change/CommentThreadsTest.java b/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
index 0c61906..83e8370 100644
--- a/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
+++ b/javatests/com/google/gerrit/server/change/CommentThreadsTest.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.entities.Comment;
 import com.google.gerrit.entities.HumanComment;
-import java.sql.Timestamp;
+import java.time.Instant;
 import org.junit.Test;
 
 public class CommentThreadsTest {
@@ -126,13 +126,15 @@
 
   @Test
   public void branchedThreadsAreFlattenedAccordingToDate() {
-    HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
-    HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
-    HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(3));
+    HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+    HumanComment sibling1 =
+        writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+    HumanComment sibling2 =
+        writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(3));
     HumanComment sibling1Child =
-        writtenOn(asReply(createComment("sibling1Child"), "sibling1"), new Timestamp(4));
+        writtenOn(asReply(createComment("sibling1Child"), "sibling1"), Instant.ofEpochMilli(4));
     HumanComment sibling2Child =
-        writtenOn(asReply(createComment("sibling2Child"), "sibling2"), new Timestamp(5));
+        writtenOn(asReply(createComment("sibling2Child"), "sibling2"), Instant.ofEpochMilli(5));
 
     ImmutableList<HumanComment> comments =
         ImmutableList.of(sibling2, sibling2Child, sibling1, sibling1Child, root);
@@ -146,9 +148,11 @@
 
   @Test
   public void threadsConsiderParentRelationshipStrongerThanDate() {
-    HumanComment root = writtenOn(createComment("root"), new Timestamp(3));
-    HumanComment child1 = writtenOn(asReply(createComment("child1"), "root"), new Timestamp(2));
-    HumanComment child2 = writtenOn(asReply(createComment("child2"), "child1"), new Timestamp(1));
+    HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(3));
+    HumanComment child1 =
+        writtenOn(asReply(createComment("child1"), "root"), Instant.ofEpochMilli(2));
+    HumanComment child2 =
+        writtenOn(asReply(createComment("child2"), "child1"), Instant.ofEpochMilli(1));
 
     ImmutableList<HumanComment> comments = ImmutableList.of(child2, child1, root);
     ImmutableSet<CommentThread<HumanComment>> commentThreads =
@@ -161,9 +165,11 @@
 
   @Test
   public void threadsFallBackToUuidOrderIfParentAndDateAreTheSame() {
-    HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
-    HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
-    HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(2));
+    HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+    HumanComment sibling1 =
+        writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+    HumanComment sibling2 =
+        writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(2));
 
     ImmutableList<HumanComment> comments = ImmutableList.of(sibling2, sibling1, root);
     ImmutableSet<CommentThread<HumanComment>> commentThreads =
@@ -224,13 +230,15 @@
 
   @Test
   public void completeThreadWithBranchesCanBeRequestedByReplyToIntermediateComment() {
-    HumanComment root = writtenOn(createComment("root"), new Timestamp(1));
-    HumanComment sibling1 = writtenOn(asReply(createComment("sibling1"), "root"), new Timestamp(2));
-    HumanComment sibling2 = writtenOn(asReply(createComment("sibling2"), "root"), new Timestamp(3));
+    HumanComment root = writtenOn(createComment("root"), Instant.ofEpochMilli(1));
+    HumanComment sibling1 =
+        writtenOn(asReply(createComment("sibling1"), "root"), Instant.ofEpochMilli(2));
+    HumanComment sibling2 =
+        writtenOn(asReply(createComment("sibling2"), "root"), Instant.ofEpochMilli(3));
     HumanComment sibling1Child =
-        writtenOn(asReply(createComment("sibling1Child"), "sibling1"), new Timestamp(4));
+        writtenOn(asReply(createComment("sibling1Child"), "sibling1"), Instant.ofEpochMilli(4));
     HumanComment sibling2Child =
-        writtenOn(asReply(createComment("sibling2Child"), "sibling2"), new Timestamp(5));
+        writtenOn(asReply(createComment("sibling2Child"), "sibling2"), Instant.ofEpochMilli(5));
 
     HumanComment reply = asReply(createComment("sibling1"), "root");
 
@@ -262,7 +270,7 @@
     return new HumanComment(
         new Comment.Key(commentUuid, "myFile", 1),
         Account.id(100),
-        new Timestamp(1234),
+        Instant.ofEpochMilli(1234),
         (short) 1,
         "Comment text",
         "serverId",
@@ -274,8 +282,8 @@
     return comment;
   }
 
-  private static HumanComment writtenOn(HumanComment comment, Timestamp writtenOn) {
-    comment.writtenOn = writtenOn;
+  private static HumanComment writtenOn(HumanComment comment, Instant writtenOn) {
+    comment.setWrittenOn(writtenOn);
     return comment;
   }
 
diff --git a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
index 8f341aa..38e50b5 100644
--- a/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
+++ b/javatests/com/google/gerrit/server/change/LabelNormalizerTest.java
@@ -213,7 +213,7 @@
     return PatchSetApproval.builder()
         .key(PatchSetApproval.key(change.currentPatchSetId(), accountId, LabelId.create(label)))
         .value(value)
-        .granted(TimeUtil.nowTs())
+        .granted(TimeUtil.now())
         .build();
   }
 
diff --git a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
index 97f6e4e..00b92b4 100644
--- a/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
+++ b/javatests/com/google/gerrit/server/events/EventDeserializerTest.java
@@ -27,8 +27,8 @@
 import com.google.gerrit.server.data.AccountAttribute;
 import com.google.gerrit.server.data.ChangeAttribute;
 import com.google.gerrit.server.data.RefUpdateAttribute;
+import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
-import java.sql.Timestamp;
 import org.junit.Test;
 
 public class EventDeserializerTest {
@@ -278,7 +278,7 @@
             Change.id(1000),
             Account.id(1000),
             BranchNameKey.create(Project.nameKey("myproject"), "mybranch"),
-            new Timestamp(System.currentTimeMillis()));
+            TimeUtil.now());
     return change;
   }
 
@@ -335,7 +335,7 @@
     a.commitMessage = "This is a test commit message";
     a.url = "http://somewhere.com";
     a.status = change.getStatus();
-    a.createdOn = change.getCreatedOn().getTime() / 1000L;
+    a.createdOn = change.getCreatedOn().getEpochSecond();
     a.wip = change.isWorkInProgress() ? true : null;
     a.isPrivate = change.isPrivate() ? true : null;
     return Suppliers.ofInstance(a);
diff --git a/javatests/com/google/gerrit/server/events/EventJsonTest.java b/javatests/com/google/gerrit/server/events/EventJsonTest.java
index 8e4f436..3c9a355 100644
--- a/javatests/com/google/gerrit/server/events/EventJsonTest.java
+++ b/javatests/com/google/gerrit/server/events/EventJsonTest.java
@@ -607,7 +607,7 @@
         Change.id(CHANGE_NUM),
         Account.id(9999),
         BranchNameKey.create(Project.nameKey(PROJECT), BRANCH),
-        TimeUtil.nowTs());
+        TimeUtil.now());
   }
 
   private <T> Supplier<T> createSupplier(T value) {
@@ -625,7 +625,7 @@
     a.commitMessage = COMMIT_MESSAGE;
     a.url = URL;
     a.status = change.getStatus();
-    a.createdOn = change.getCreatedOn().getTime() / 1000L;
+    a.createdOn = change.getCreatedOn().getEpochSecond();
     a.wip = change.isWorkInProgress() ? true : null;
     a.isPrivate = change.isPrivate() ? true : null;
     return Suppliers.ofInstance(a);
diff --git a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
index 3a8d7e4..29dbe58 100644
--- a/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
+++ b/javatests/com/google/gerrit/server/git/DeleteZombieCommentsRefsTest.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Date;
 import java.util.List;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -203,11 +204,14 @@
     return repo.exactRef(refName);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private static ObjectId createCommit(Repository repo, ObjectId treeId, ObjectId parentCommit)
       throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter()) {
       PersonIdent committer =
-          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(treeId);
       cb.setCommitter(committer);
diff --git a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
index 4902830..6c771d7 100644
--- a/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
+++ b/javatests/com/google/gerrit/server/git/MultiBaseLocalDiskRepositoryManagerTest.java
@@ -26,7 +26,7 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.SortedSet;
+import java.util.NavigableSet;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -77,7 +77,7 @@
     assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
         .isEqualTo(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString());
 
-    SortedSet<Project.NameKey> repoList = repoManager.list();
+    NavigableSet<Project.NameKey> repoList = repoManager.list();
     assertThat(repoList).hasSize(1);
     assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
         .isEqualTo(new Project.NameKey[] {someProjectKey});
@@ -105,7 +105,7 @@
     assertThat(repoManager.getBasePath(someProjectKey).toAbsolutePath().toString())
         .isEqualTo(alternateBasePath.toString());
 
-    SortedSet<Project.NameKey> repoList = repoManager.list();
+    NavigableSet<Project.NameKey> repoList = repoManager.list();
     assertThat(repoList).hasSize(1);
     assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
         .isEqualTo(new Project.NameKey[] {someProjectKey});
@@ -131,7 +131,7 @@
     createRepository(repoManager.getBasePath(basePathProject), misplacedProject2);
     createRepository(alternateBasePath, misplacedProject1);
 
-    SortedSet<Project.NameKey> repoList = repoManager.list();
+    NavigableSet<Project.NameKey> repoList = repoManager.list();
     assertThat(repoList).hasSize(2);
     assertThat(repoList.toArray(new Project.NameKey[repoList.size()]))
         .isEqualTo(new Project.NameKey[] {altPathProject, basePathProject});
diff --git a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
index 690a5cc..6792703 100644
--- a/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
+++ b/javatests/com/google/gerrit/server/git/meta/VersionedMetaDataTest.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.testing.TestTimeUtil;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.Optional;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
@@ -220,9 +221,13 @@
     return u;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private CommitBuilder newCommitBuilder() {
     CommitBuilder cb = new CommitBuilder();
-    PersonIdent author = new PersonIdent("J. Author", "author@example.com", TimeUtil.nowTs(), TZ);
+    PersonIdent author =
+        new PersonIdent("J. Author", "author@example.com", Date.from(TimeUtil.now()), TZ);
     cb.setAuthor(author);
     cb.setCommitter(
         new PersonIdent(
diff --git a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
index e24d481..54407ca 100644
--- a/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AbstractGroupTest.java
@@ -30,7 +30,8 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.InMemoryRepositoryManager;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
 import java.util.Optional;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -59,13 +60,16 @@
   protected Account.Id userId;
   protected PersonIdent userIdent;
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Before
   public void abstractGroupTestSetUp() throws Exception {
     allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
     repoManager = new InMemoryRepositoryManager();
     allUsersRepo = repoManager.createRepository(allUsersName);
     serverAccountId = Account.id(SERVER_ACCOUNT_NUMBER);
-    serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+    serverIdent = new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
     userId = Account.id(USER_ACCOUNT_NUMBER);
     userIdent = newPersonIdent(userId, serverIdent);
   }
@@ -75,12 +79,15 @@
     allUsersRepo.close();
   }
 
-  protected Timestamp getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
+  protected Instant getTipTimestamp(AccountGroup.UUID uuid) throws Exception {
     try (RevWalk rw = new RevWalk(allUsersRepo)) {
       Ref ref = allUsersRepo.exactRef(RefNames.refsGroups(uuid));
       return ref == null
           ? null
-          : new Timestamp(rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().getTime());
+          : rw.parseCommit(ref.getObjectId()).getAuthorIdent().getWhen().toInstant();
     }
   }
 
@@ -109,8 +116,11 @@
     return md;
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   protected static PersonIdent newPersonIdent() {
-    return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+    return new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
   }
 
   protected static PersonIdent newPersonIdent(Account.Id id, PersonIdent ident) {
@@ -123,7 +133,7 @@
   }
 
   private static Optional<Account> getAccount(Account.Id id) {
-    Account.Builder account = Account.builder(id, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(id, TimeUtil.now());
     account.setFullName("Account " + id);
     return Optional.of(account.build());
   }
diff --git a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
index 6ad899e..a764654 100644
--- a/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
+++ b/javatests/com/google/gerrit/server/group/db/AuditLogReaderTest.java
@@ -24,7 +24,7 @@
 import com.google.gerrit.entities.AccountGroupMemberAudit;
 import com.google.gerrit.entities.InternalGroup;
 import com.google.gerrit.server.account.GroupUuid;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Set;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.junit.Before;
@@ -299,7 +299,7 @@
   }
 
   private static AccountGroupMemberAudit createExpMemberAudit(
-      AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Timestamp addedOn) {
+      AccountGroup.Id groupId, Account.Id id, Account.Id addedBy, Instant addedOn) {
     return AccountGroupMemberAudit.builder()
         .groupId(groupId)
         .memberId(id)
@@ -309,7 +309,7 @@
   }
 
   private static AccountGroupByIdAudit createExpGroupAudit(
-      AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Timestamp addedOn) {
+      AccountGroup.Id groupId, AccountGroup.UUID uuid, Account.Id addedBy, Instant addedOn) {
     return AccountGroupByIdAudit.builder()
         .groupId(groupId)
         .includeUuid(uuid)
diff --git a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
index 31b2bc3..dbe255c 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupConfigTest.java
@@ -37,11 +37,12 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.IOException;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.Month;
 import java.time.ZoneId;
+import java.util.Date;
 import java.util.Optional;
 import java.util.TimeZone;
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -245,7 +246,7 @@
   @Test
   public void createdOnDefaultsToNow() throws Exception {
     // Git timestamps are only precise to the second.
-    Timestamp testStart = TimeUtil.truncateToSecond(TimeUtil.nowTs());
+    Instant testStart = TimeUtil.truncateToSecond(TimeUtil.now());
 
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
@@ -261,7 +262,7 @@
 
   @Test
   public void specifiedCreatedOnIsRespectedForNewGroup() throws Exception {
-    Timestamp createdOn = toTimestamp(LocalDate.of(2017, Month.DECEMBER, 11).atTime(13, 44, 10));
+    Instant createdOn = toInstant(LocalDate.of(2017, Month.DECEMBER, 11).atTime(13, 44, 10));
 
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     GroupDelta groupDelta = GroupDelta.builder().setUpdatedOn(createdOn).build();
@@ -604,8 +605,8 @@
 
   @Test
   public void createdOnIsNotAffectedByFurtherUpdates() throws Exception {
-    Timestamp createdOn = toTimestamp(LocalDate.of(2017, Month.MAY, 11).atTime(13, 44, 10));
-    Timestamp updatedOn = toTimestamp(LocalDate.of(2017, Month.DECEMBER, 12).atTime(10, 21, 49));
+    Instant createdOn = toInstant(LocalDate.of(2017, Month.MAY, 11).atTime(13, 44, 10));
+    Instant updatedOn = toInstant(LocalDate.of(2017, Month.DECEMBER, 12).atTime(10, 21, 49));
 
     InternalGroupCreation groupCreation = getPrefilledGroupCreationBuilder().build();
     GroupDelta initialGroupDelta = GroupDelta.builder().setUpdatedOn(createdOn).build();
@@ -737,7 +738,7 @@
             .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
             .setName(AccountGroup.nameKey("Another name"))
-            .setUpdatedOn(new Timestamp(92900892))
+            .setUpdatedOn(Instant.ofEpochMilli(92900892))
             .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
             .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
@@ -758,7 +759,7 @@
             .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
             .setName(AccountGroup.nameKey("Another name"))
-            .setUpdatedOn(new Timestamp(92900892))
+            .setUpdatedOn(Instant.ofEpochMilli(92900892))
             .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
             .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
@@ -780,7 +781,7 @@
             .setOwnerGroupUUID(AccountGroup.uuid("another owner"))
             .setVisibleToAll(true)
             .setName(AccountGroup.nameKey("Another name"))
-            .setUpdatedOn(new Timestamp(92900892))
+            .setUpdatedOn(Instant.ofEpochMilli(92900892))
             .setMemberModification(members -> ImmutableSet.of(Account.id(1), Account.id(2)))
             .setSubgroupModification(subgroups -> ImmutableSet.of(AccountGroup.uuid("subgroup")))
             .build();
@@ -864,7 +865,7 @@
   public void newCommitIsNotCreatedForPureUpdatedOnUpdate() throws Exception {
     createArbitraryGroup(groupUuid);
 
-    Timestamp updatedOn = toTimestamp(LocalDate.of(3017, Month.DECEMBER, 12).atTime(10, 21, 49));
+    Instant updatedOn = toInstant(LocalDate.of(3017, Month.DECEMBER, 12).atTime(10, 21, 49));
     GroupDelta groupDelta = GroupDelta.builder().setUpdatedOn(updatedOn).build();
 
     RevCommit commitBeforeUpdate = getLatestCommitForGroup(groupUuid);
@@ -1005,7 +1006,7 @@
   @Test
   public void commitTimeMatchesDefaultCreatedOnOfNewGroup() throws Exception {
     // Git timestamps are only precise to the second.
-    long testStartAsSecondsSinceEpoch = TimeUtil.nowTs().getTime() / 1000;
+    long testStartAsSecondsSinceEpoch = TimeUtil.now().getEpochSecond();
 
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
@@ -1032,7 +1033,7 @@
             .build();
     GroupDelta groupDelta =
         GroupDelta.builder()
-            .setUpdatedOn(new Timestamp(createdOnAsSecondsSinceEpoch * 1000))
+            .setUpdatedOn(Instant.ofEpochSecond(createdOnAsSecondsSinceEpoch))
             .build();
     createGroup(groupCreation, groupDelta);
 
@@ -1040,11 +1041,14 @@
     assertThat(revCommit.getCommitTime()).isEqualTo(createdOnAsSecondsSinceEpoch);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void timestampOfCommitterMatchesSpecifiedCreatedOnOfNewGroup() throws Exception {
-    Timestamp committerTimestamp =
-        toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
-    Timestamp createdOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+    Instant committerTimestamp =
+        toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+    Instant createdOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
 
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
@@ -1061,23 +1065,27 @@
     groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
 
     PersonIdent committerIdent =
-        new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, timeZone);
+        new PersonIdent(
+            "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
       metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
       groupConfig.commit(metaDataUpdate);
     }
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
-    assertThat(revCommit.getCommitterIdent().getWhen().getTime()).isEqualTo(createdOn.getTime());
+    assertThat(revCommit.getCommitterIdent().getWhen().getTime())
+        .isEqualTo(createdOn.toEpochMilli());
     assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
         .isEqualTo(timeZone.getRawOffset());
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void timestampOfAuthorMatchesSpecifiedCreatedOnOfNewGroup() throws Exception {
-    Timestamp authorTimestamp =
-        toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
-    Timestamp createdOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+    Instant authorTimestamp = toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+    Instant createdOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
 
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
@@ -1094,14 +1102,14 @@
     groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
 
     PersonIdent authorIdent =
-        new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, timeZone);
+        new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
       metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
       groupConfig.commit(metaDataUpdate);
     }
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
-    assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(createdOn.getTime());
+    assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(createdOn.toEpochMilli());
     assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
         .isEqualTo(timeZone.getRawOffset());
   }
@@ -1109,7 +1117,7 @@
   @Test
   public void commitTimeMatchesDefaultUpdatedOnOfUpdatedGroup() throws Exception {
     // Git timestamps are only precise to the second.
-    long testStartAsSecondsSinceEpoch = TimeUtil.nowTs().getTime() / 1000;
+    long testStartAsSecondsSinceEpoch = TimeUtil.now().getEpochSecond();
 
     createArbitraryGroup(groupUuid);
     GroupDelta groupDelta =
@@ -1129,7 +1137,7 @@
     GroupDelta groupDelta =
         GroupDelta.builder()
             .setName(AccountGroup.nameKey("Another name"))
-            .setUpdatedOn(new Timestamp(updatedOnAsSecondsSinceEpoch * 1000))
+            .setUpdatedOn(Instant.ofEpochSecond(updatedOnAsSecondsSinceEpoch))
             .build();
     updateGroup(groupUuid, groupDelta);
 
@@ -1138,10 +1146,13 @@
   }
 
   @Test
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public void timestampOfCommitterMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
-    Timestamp committerTimestamp =
-        toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
-    Timestamp updatedOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+    Instant committerTimestamp =
+        toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+    Instant updatedOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
 
     createArbitraryGroup(groupUuid);
     GroupDelta groupDelta =
@@ -1153,23 +1164,27 @@
     groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
 
     PersonIdent committerIdent =
-        new PersonIdent("Jane", "Jane@gerritcodereview.com", committerTimestamp, timeZone);
+        new PersonIdent(
+            "Jane", "Jane@gerritcodereview.com", Date.from(committerTimestamp), timeZone);
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
       metaDataUpdate.getCommitBuilder().setCommitter(committerIdent);
       groupConfig.commit(metaDataUpdate);
     }
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
-    assertThat(revCommit.getCommitterIdent().getWhen().getTime()).isEqualTo(updatedOn.getTime());
+    assertThat(revCommit.getCommitterIdent().getWhen().getTime())
+        .isEqualTo(updatedOn.toEpochMilli());
     assertThat(revCommit.getCommitterIdent().getTimeZone().getRawOffset())
         .isEqualTo(timeZone.getRawOffset());
   }
 
   @Test
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public void timestampOfAuthorMatchesSpecifiedUpdatedOnOfUpdatedGroup() throws Exception {
-    Timestamp authorTimestamp =
-        toTimestamp(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
-    Timestamp updatedOn = toTimestamp(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
+    Instant authorTimestamp = toInstant(LocalDate.of(2017, Month.DECEMBER, 13).atTime(15, 5, 27));
+    Instant updatedOn = toInstant(LocalDate.of(2016, Month.MARCH, 11).atTime(23, 49, 11));
 
     createArbitraryGroup(groupUuid);
     GroupDelta groupDelta =
@@ -1181,14 +1196,14 @@
     groupConfig.setGroupDelta(groupDelta, auditLogFormatter);
 
     PersonIdent authorIdent =
-        new PersonIdent("Jane", "Jane@gerritcodereview.com", authorTimestamp, timeZone);
+        new PersonIdent("Jane", "Jane@gerritcodereview.com", Date.from(authorTimestamp), timeZone);
     try (MetaDataUpdate metaDataUpdate = createMetaDataUpdate()) {
       metaDataUpdate.getCommitBuilder().setAuthor(authorIdent);
       groupConfig.commit(metaDataUpdate);
     }
 
     RevCommit revCommit = getLatestCommitForGroup(groupUuid);
-    assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(updatedOn.getTime());
+    assertThat(revCommit.getAuthorIdent().getWhen().getTime()).isEqualTo(updatedOn.toEpochMilli());
     assertThat(revCommit.getAuthorIdent().getTimeZone().getRawOffset())
         .isEqualTo(timeZone.getRawOffset());
   }
@@ -1455,8 +1470,8 @@
                 + "Rename from Old name to New name");
   }
 
-  private static Timestamp toTimestamp(LocalDateTime localDateTime) {
-    return Timestamp.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant());
+  private static Instant toInstant(LocalDateTime localDateTime) {
+    return localDateTime.atZone(ZoneId.systemDefault()).toInstant();
   }
 
   private void populateGroupConfig(AccountGroup.UUID uuid, String fileContent) throws Exception {
@@ -1539,10 +1554,13 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private MetaDataUpdate createMetaDataUpdate() {
     PersonIdent serverIdent =
         new PersonIdent(
-            "Gerrit Server", "noreply@gerritcodereview.com", TimeUtil.nowTs(), timeZone);
+            "Gerrit Server", "noreply@gerritcodereview.com", Date.from(TimeUtil.now()), timeZone);
 
     MetaDataUpdate metaDataUpdate =
         new MetaDataUpdate(
@@ -1564,7 +1582,7 @@
   }
 
   private static Account createAccount(Account.Id id, String name) {
-    Account.Builder account = Account.builder(id, TimeUtil.nowTs());
+    Account.Builder account = Account.builder(id, TimeUtil.now());
     account.setFullName(name);
     return account.build();
   }
diff --git a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
index 3b7beb9..afc56ff 100644
--- a/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
+++ b/javatests/com/google/gerrit/server/group/db/GroupNameNotesTest.java
@@ -45,6 +45,7 @@
 import com.google.gerrit.truth.OptionalSubject;
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.List;
 import java.util.Optional;
 import java.util.TimeZone;
@@ -557,8 +558,11 @@
     return GroupReference.create(AccountGroup.uuid(name + "-" + id), name);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private static PersonIdent newPersonIdent() {
-    return new PersonIdent(SERVER_NAME, SERVER_EMAIL, TimeUtil.nowTs(), TZ);
+    return new PersonIdent(SERVER_NAME, SERVER_EMAIL, Date.from(TimeUtil.now()), TZ);
   }
 
   private static ObjectId getNoteKey(GroupReference g) {
diff --git a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
index d16efc3..4d9cb76 100644
--- a/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/account/AccountFieldTest.java
@@ -36,7 +36,7 @@
   @Test
   public void refStateFieldValues() throws Exception {
     AllUsersName allUsersName = new AllUsersName(AllUsersNameProvider.DEFAULT);
-    Account.Builder account = Account.builder(Account.id(1), TimeUtil.nowTs());
+    Account.Builder account = Account.builder(Account.id(1), TimeUtil.now());
     String metaId = "0e39795bb25dc914118224995c53c5c36923a461";
     account.setMetaId(metaId);
     List<String> values =
@@ -51,7 +51,7 @@
   public void externalIdStateFieldValues() throws Exception {
     Account.Id id = Account.id(1);
     Account account =
-        Account.builder(id, TimeUtil.nowTs())
+        Account.builder(id, TimeUtil.now())
             .setMetaId("1234567812345678123456781234567812345678")
             .build();
     ExternalId extId1 =
diff --git a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
index 6ad2060..dc1440b 100644
--- a/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
+++ b/javatests/com/google/gerrit/server/index/change/ChangeFieldTest.java
@@ -33,7 +33,7 @@
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gerrit.testing.TestTimeUtil;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
@@ -55,17 +55,22 @@
 
   @Test
   public void reviewerFieldValues() {
-    Table<ReviewerStateInternal, Account.Id, Timestamp> t = HashBasedTable.create();
-    Timestamp t1 = TimeUtil.nowTs();
+    Table<ReviewerStateInternal, Account.Id, Instant> t = HashBasedTable.create();
+
+    // Timestamps are stored as epoch millis in the reviewer field. Epoch millis are less precise
+    // than Instants which have nanosecond precision. Create Instants with millisecond precision
+    // here so that the comparison for the assertions works.
+    Instant t1 = Instant.ofEpochMilli(TimeUtil.nowMs());
+    Instant t2 = Instant.ofEpochMilli(TimeUtil.nowMs());
+
     t.put(ReviewerStateInternal.REVIEWER, Account.id(1), t1);
-    Timestamp t2 = TimeUtil.nowTs();
     t.put(ReviewerStateInternal.CC, Account.id(2), t2);
     ReviewerSet reviewers = ReviewerSet.fromTable(t);
 
     List<String> values = ChangeField.getReviewerFieldValues(reviewers);
     assertThat(values)
         .containsExactly(
-            "REVIEWER,1", "REVIEWER,1," + t1.getTime(), "CC,2", "CC,2," + t2.getTime());
+            "REVIEWER,1", "REVIEWER,1," + t1.toEpochMilli(), "CC,2", "CC,2," + t2.toEpochMilli());
 
     assertThat(ChangeField.parseReviewerFieldValues(Change.id(1), values)).isEqualTo(reviewers);
   }
diff --git a/javatests/com/google/gerrit/server/mail/SignedTokenTest.java b/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
index 41d8d69..27c4f56 100644
--- a/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
+++ b/javatests/com/google/gerrit/server/mail/SignedTokenTest.java
@@ -152,7 +152,7 @@
   private static String randomString(int length) {
     String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
     Random random = new Random();
-    StringBuffer sb = new StringBuffer();
+    StringBuilder sb = new StringBuilder();
     for (int i = 0; i < length; i++) {
       int number = random.nextInt(62);
       sb.append(str.charAt(number));
diff --git a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
index 5980071..629b0cc 100644
--- a/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
+++ b/javatests/com/google/gerrit/server/mail/send/FromAddressGeneratorProviderTest.java
@@ -362,7 +362,7 @@
 
   private AccountState makeUser(String name, String email) {
     final Account.Id userId = Account.id(42);
-    final Account.Builder account = Account.builder(userId, TimeUtil.nowTs());
+    final Account.Builder account = Account.builder(userId, TimeUtil.now());
     account.setFullName(name);
     account.setPreferredEmail(email);
     return AccountState.forAccount(account.build());
diff --git a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
index a2a9b7d..222be83 100644
--- a/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/AbstractChangeNotesTest.java
@@ -67,7 +67,8 @@
 import com.google.inject.Guice;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.Date;
 import java.util.TimeZone;
 import java.util.concurrent.ExecutorService;
 import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
@@ -114,22 +115,26 @@
   protected Injector injector;
   private String systemTimeZone;
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Before
   public void setUpTestEnvironment() throws Exception {
     setTimeForTesting();
 
-    serverIdent = new PersonIdent("Gerrit Server", "noreply@gerrit.com", TimeUtil.nowTs(), TZ);
+    serverIdent =
+        new PersonIdent("Gerrit Server", "noreply@gerrit.com", Date.from(TimeUtil.now()), TZ);
     project = Project.nameKey("test-project");
     repoManager = new InMemoryRepositoryManager();
     repo = repoManager.createRepository(project);
     tr = new TestRepository<>(repo);
     rw = tr.getRevWalk();
     accountCache = new FakeAccountCache();
-    Account.Builder co = Account.builder(Account.id(1), TimeUtil.nowTs());
+    Account.Builder co = Account.builder(Account.id(1), TimeUtil.now());
     co.setFullName("Change Owner");
     co.setPreferredEmail("change@owner.com");
     accountCache.put(co.build());
-    Account.Builder ou = Account.builder(Account.id(2), TimeUtil.nowTs());
+    Account.Builder ou = Account.builder(Account.id(2), TimeUtil.now());
     ou.setFullName("Other Account");
     ou.setPreferredEmail("other@account.com");
     accountCache.put(ou.build());
@@ -265,7 +270,7 @@
       int line,
       IdentifiedUser commenter,
       String parentUUID,
-      Timestamp t,
+      Instant t,
       String message,
       short side,
       ObjectId commitId,
@@ -286,11 +291,11 @@
     return c;
   }
 
-  protected static Timestamp truncate(Timestamp ts) {
-    return new Timestamp((ts.getTime() / 1000) * 1000);
+  protected static Instant truncate(Instant ts) {
+    return Instant.ofEpochMilli((ts.toEpochMilli() / 1000) * 1000);
   }
 
-  protected static Timestamp after(Change c, long millis) {
-    return new Timestamp(c.getCreatedOn().getTime() + millis);
+  protected static Instant after(Change c, long millis) {
+    return Instant.ofEpochMilli(c.getCreatedOn().toEpochMilli() + millis);
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
index f105cf1..666b8fc 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesCommitTest.java
@@ -106,7 +106,7 @@
     ChangeNotes notes = newNotes(change).load();
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     PersonIdent author =
-        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent);
+        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent);
     try (ObjectInserter ins = testRepo.getRepository().newObjectInserter()) {
       CommitBuilder cb = new CommitBuilder();
       cb.setParentId(notes.getRevision());
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
index 548764b..e557277 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesParserTest.java
@@ -712,7 +712,7 @@
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
         body,
-        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent),
         false);
   }
 
@@ -724,7 +724,7 @@
     ChangeNoteUtil noteUtil = injector.getInstance(ChangeNoteUtil.class);
     return writeCommit(
         body,
-        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.nowTs(), serverIdent),
+        noteUtil.newAccountIdIdent(changeOwner.getAccount().id(), TimeUtil.now(), serverIdent),
         initWorkInProgress);
   }
 
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
index d9846ce..3295828 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesStateTest.java
@@ -92,8 +92,8 @@
     cols =
         ChangeColumns.builder()
             .changeKey(Change.key(CHANGE_KEY))
-            .createdOn(new Timestamp(123456L))
-            .lastUpdatedOn(new Timestamp(234567L))
+            .createdOn(Instant.ofEpochMilli(123456L))
+            .lastUpdatedOn(Instant.ofEpochMilli(234567L))
             .owner(Account.id(1000))
             .branch("refs/heads/master")
             .subject("Test change")
@@ -135,7 +135,9 @@
   @Test
   public void serializeCreatedOn() throws Exception {
     assertRoundTrip(
-        newBuilder().columns(cols.toBuilder().createdOn(new Timestamp(98765L)).build()).build(),
+        newBuilder()
+            .columns(cols.toBuilder().createdOn(Instant.ofEpochMilli(98765L)).build())
+            .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -146,7 +148,9 @@
   @Test
   public void serializeLastUpdatedOn() throws Exception {
     assertRoundTrip(
-        newBuilder().columns(cols.toBuilder().lastUpdatedOn(new Timestamp(98765L)).build()).build(),
+        newBuilder()
+            .columns(cols.toBuilder().lastUpdatedOn(Instant.ofEpochMilli(98765L)).build())
+            .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -376,7 +380,7 @@
                     PatchSet.id(ID, 1), Account.id(2001), LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
             .tag("tag")
-            .granted(new Timestamp(1212L))
+            .granted(Instant.ofEpochMilli(1212L))
             .build();
     Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
     ByteString a1Bytes = Protos.toByteString(psa1);
@@ -389,7 +393,7 @@
             .value(-1)
             .tag("tag")
             .copied(true)
-            .granted(new Timestamp(3434L))
+            .granted(Instant.ofEpochMilli(3434L))
             .build();
     Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
     ByteString a2Bytes = Protos.toByteString(psa2);
@@ -419,7 +423,7 @@
             .uuid(Optional.of(PatchSetApproval.uuid("577fb248e474018276351785930358ec0450e9f7")))
             .value(1)
             .tag("tag")
-            .granted(new Timestamp(1212L))
+            .granted(Instant.ofEpochMilli(1212L))
             .build();
     Entities.PatchSetApproval psa1 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a1);
     ByteString a1Bytes = Protos.toByteString(psa1);
@@ -433,7 +437,7 @@
             .value(-1)
             .tag("tag")
             .copied(true)
-            .granted(new Timestamp(3434L))
+            .granted(Instant.ofEpochMilli(3434L))
             .build();
     Entities.PatchSetApproval psa2 = PatchSetApprovalProtoConverter.INSTANCE.toProto(a2);
     ByteString a2Bytes = Protos.toByteString(psa2);
@@ -459,9 +463,13 @@
         newBuilder()
             .reviewers(
                 ReviewerSet.fromTable(
-                    ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                        .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
-                        .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
+                    ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
+                        .put(
+                            ReviewerStateInternal.CC, Account.id(2001), Instant.ofEpochMilli(1212L))
+                        .put(
+                            ReviewerStateInternal.REVIEWER,
+                            Account.id(2002),
+                            Instant.ofEpochMilli(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -487,15 +495,15 @@
         newBuilder()
             .reviewersByEmail(
                 ReviewerByEmailSet.fromTable(
-                    ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+                    ImmutableTable.<ReviewerStateInternal, Address, Instant>builder()
                         .put(
                             ReviewerStateInternal.CC,
                             Address.create("Name1", "email1@example.com"),
-                            new Timestamp(1212L))
+                            Instant.ofEpochMilli(1212L))
                         .put(
                             ReviewerStateInternal.REVIEWER,
                             Address.create("Name2", "email2@example.com"),
-                            new Timestamp(3434L))
+                            Instant.ofEpochMilli(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -525,7 +533,7 @@
                         ImmutableTable.of(
                             ReviewerStateInternal.CC,
                             Address.create("emailonly@example.com"),
-                            new Timestamp(1212L))))
+                            Instant.ofEpochMilli(1212L))))
                 .build(),
             ChangeNotesStateProto.newBuilder()
                 .setMetaId(SHA_BYTES)
@@ -553,9 +561,13 @@
         newBuilder()
             .pendingReviewers(
                 ReviewerSet.fromTable(
-                    ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
-                        .put(ReviewerStateInternal.CC, Account.id(2001), new Timestamp(1212L))
-                        .put(ReviewerStateInternal.REVIEWER, Account.id(2002), new Timestamp(3434L))
+                    ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
+                        .put(
+                            ReviewerStateInternal.CC, Account.id(2001), Instant.ofEpochMilli(1212L))
+                        .put(
+                            ReviewerStateInternal.REVIEWER,
+                            Account.id(2002),
+                            Instant.ofEpochMilli(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -581,15 +593,15 @@
         newBuilder()
             .pendingReviewersByEmail(
                 ReviewerByEmailSet.fromTable(
-                    ImmutableTable.<ReviewerStateInternal, Address, Timestamp>builder()
+                    ImmutableTable.<ReviewerStateInternal, Address, Instant>builder()
                         .put(
                             ReviewerStateInternal.CC,
                             Address.create("Name1", "email1@example.com"),
-                            new Timestamp(1212L))
+                            Instant.ofEpochMilli(1212L))
                         .put(
                             ReviewerStateInternal.REVIEWER,
                             Address.create("Name2", "email2@example.com"),
-                            new Timestamp(3434L))
+                            Instant.ofEpochMilli(3434L))
                         .build()))
             .build(),
         ChangeNotesStateProto.newBuilder()
@@ -629,12 +641,12 @@
             .reviewerUpdates(
                 ImmutableList.of(
                     ReviewerStatusUpdate.create(
-                        new Timestamp(1212L),
+                        Instant.ofEpochMilli(1212L),
                         Account.id(1000),
                         Account.id(2002),
                         ReviewerStateInternal.CC),
                     ReviewerStatusUpdate.create(
-                        new Timestamp(3434L),
+                        Instant.ofEpochMilli(3434L),
                         Account.id(1000),
                         Account.id(2001),
                         ReviewerStateInternal.REVIEWER)))
@@ -796,9 +808,11 @@
             .assigneeUpdates(
                 ImmutableList.of(
                     AssigneeStatusUpdate.create(
-                        new Timestamp(1212L), Account.id(1000), Optional.of(Account.id(2001))),
+                        Instant.ofEpochMilli(1212L),
+                        Account.id(1000),
+                        Optional.of(Account.id(2001))),
                     AssigneeStatusUpdate.create(
-                        new Timestamp(3434L), Account.id(1000), Optional.empty())))
+                        Instant.ofEpochMilli(3434L), Account.id(1000), Optional.empty())))
             .build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
@@ -843,7 +857,7 @@
         ChangeMessage.create(
             ChangeMessage.key(ID, "uuid1"),
             Account.id(1000),
-            new Timestamp(1212L),
+            Instant.ofEpochMilli(1212L),
             PatchSet.id(ID, 1));
     Entities.ChangeMessage m1Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m1);
     ByteString m1Bytes = Protos.toByteString(m1Proto);
@@ -853,7 +867,7 @@
         ChangeMessage.create(
             ChangeMessage.key(ID, "uuid2"),
             Account.id(2000),
-            new Timestamp(3434L),
+            Instant.ofEpochMilli(3434L),
             PatchSet.id(ID, 2));
     Entities.ChangeMessage m2Proto = ChangeMessageProtoConverter.INSTANCE.toProto(m2);
     ByteString m2Bytes = Protos.toByteString(m2Proto);
@@ -877,7 +891,7 @@
         new HumanComment(
             new Comment.Key("uuid1", "file1", 1),
             Account.id(1001),
-            new Timestamp(1212L),
+            Instant.ofEpochMilli(1212L),
             (short) 1,
             "message 1",
             "serverId",
@@ -889,7 +903,7 @@
         new HumanComment(
             new Comment.Key("uuid2", "file2", 2),
             Account.id(1002),
-            new Timestamp(3434L),
+            Instant.ofEpochMilli(3434L),
             (short) 2,
             "message 2",
             "serverId",
@@ -965,7 +979,7 @@
                     "submitRequirementsResult",
                     new TypeLiteral<ImmutableList<SubmitRequirementResult>>() {}.getType())
                 .put("updateCount", int.class)
-                .put("mergedOn", Timestamp.class)
+                .put("mergedOn", Instant.class)
                 .build());
   }
 
@@ -975,8 +989,8 @@
         .hasAutoValueMethods(
             ImmutableMap.<String, Type>builder()
                 .put("changeKey", Change.Key.class)
-                .put("createdOn", Timestamp.class)
-                .put("lastUpdatedOn", Timestamp.class)
+                .put("createdOn", Instant.class)
+                .put("lastUpdatedOn", Instant.class)
                 .put("owner", Account.Id.class)
                 .put("branch", String.class)
                 .put("currentPatchSetId", PatchSet.Id.class)
@@ -1002,7 +1016,7 @@
                 .put("id", PatchSet.Id.class)
                 .put("commitId", ObjectId.class)
                 .put("uploader", Account.Id.class)
-                .put("createdOn", Timestamp.class)
+                .put("createdOn", Instant.class)
                 .put("groups", new TypeLiteral<ImmutableList<String>>() {}.getType())
                 .put("pushCertificate", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("description", new TypeLiteral<Optional<String>>() {}.getType())
@@ -1024,7 +1038,7 @@
                 .put("key", PatchSetApproval.Key.class)
                 .put("uuid", new TypeLiteral<Optional<PatchSetApproval.UUID>>() {}.getType())
                 .put("value", short.class)
-                .put("granted", Timestamp.class)
+                .put("granted", Instant.class)
                 .put("tag", new TypeLiteral<Optional<String>>() {}.getType())
                 .put("realAccountId", Account.Id.class)
                 .put("postSubmit", boolean.class)
@@ -1040,7 +1054,7 @@
             ImmutableMap.of(
                 "table",
                 new TypeLiteral<
-                    ImmutableTable<ReviewerStateInternal, Account.Id, Timestamp>>() {}.getType(),
+                    ImmutableTable<ReviewerStateInternal, Account.Id, Instant>>() {}.getType(),
                 "accounts",
                 new TypeLiteral<ImmutableSet<Account.Id>>() {}.getType()));
   }
@@ -1052,7 +1066,7 @@
             ImmutableMap.of(
                 "table",
                 new TypeLiteral<
-                    ImmutableTable<ReviewerStateInternal, Address, Timestamp>>() {}.getType(),
+                    ImmutableTable<ReviewerStateInternal, Address, Instant>>() {}.getType(),
                 "users",
                 new TypeLiteral<ImmutableSet<Address>>() {}.getType()));
   }
@@ -1062,7 +1076,7 @@
     assertThatSerializedClass(ReviewerStatusUpdate.class)
         .hasAutoValueMethods(
             ImmutableMap.of(
-                "date", Timestamp.class,
+                "date", Instant.class,
                 "updatedBy", Account.Id.class,
                 "reviewer", Account.Id.class,
                 "state", ReviewerStateInternal.class));
@@ -1074,7 +1088,7 @@
         .hasAutoValueMethods(
             ImmutableMap.of(
                 "date",
-                Timestamp.class,
+                Instant.class,
                 "updatedBy",
                 Account.Id.class,
                 "currentAssignee",
@@ -1112,7 +1126,7 @@
   @Test
   public void serializeMergedOn() throws Exception {
     assertRoundTrip(
-        newBuilder().mergedOn(new Timestamp(234567L)).build(),
+        newBuilder().mergedOn(Instant.ofEpochMilli(234567L)).build(),
         ChangeNotesStateProto.newBuilder()
             .setMetaId(SHA_BYTES)
             .setChangeId(ID.get())
@@ -1131,7 +1145,7 @@
             ImmutableMap.<String, Type>builder()
                 .put("key", ChangeMessage.Key.class)
                 .put("author", Account.Id.class)
-                .put("writtenOn", Timestamp.class)
+                .put("writtenOn", Instant.class)
                 .put("message", String.class)
                 .put("patchset", PatchSet.Id.class)
                 .put("tag", String.class)
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 38daae6..09c8059 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -62,7 +62,6 @@
 import com.google.gerrit.server.validators.ValidationException;
 import com.google.gerrit.testing.TestChanges;
 import com.google.inject.Inject;
-import java.sql.Timestamp;
 import java.time.Instant;
 import java.util.LinkedHashSet;
 import java.util.List;
@@ -132,7 +131,7 @@
             1,
             changeOwner,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "Comment",
             (short) 1,
             commit,
@@ -194,7 +193,7 @@
             1,
             changeOwner,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "Comment",
             (short) 1,
             commit,
@@ -923,7 +922,7 @@
                     LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .tag("tag")
             .realAccountId(otherUserId)
             .build());
@@ -975,7 +974,7 @@
                     LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .build());
     update.commit();
 
@@ -1008,7 +1007,7 @@
                     LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .tag(strangeTag)
             .build());
     update.commit();
@@ -1037,7 +1036,7 @@
                     LabelId.create(LabelId.CODE_REVIEW)))
             .value(1)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .build());
     update.putCopiedApproval(
         PatchSetApproval.builder()
@@ -1048,7 +1047,7 @@
                     LabelId.create(LabelId.VERIFIED)))
             .value(1)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .build());
     update.commit();
 
@@ -1073,7 +1072,7 @@
                     LabelId.create(LabelId.CODE_REVIEW)))
             .value(2)
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .build());
     update.commit();
 
@@ -1097,11 +1096,11 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    Instant ts = update.getWhen();
     assertThat(notes.getReviewers())
         .isEqualTo(
             ReviewerSet.fromTable(
-                ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+                ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
                     .put(REVIEWER, Account.id(1), ts)
                     .put(REVIEWER, Account.id(2), ts)
                     .build()));
@@ -1116,11 +1115,11 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    Instant ts = update.getWhen();
     assertThat(notes.getReviewers())
         .isEqualTo(
             ReviewerSet.fromTable(
-                ImmutableTable.<ReviewerStateInternal, Account.Id, Timestamp>builder()
+                ImmutableTable.<ReviewerStateInternal, Account.Id, Instant>builder()
                     .put(REVIEWER, Account.id(1), ts)
                     .put(CC, Account.id(2), ts)
                     .build()));
@@ -1134,7 +1133,7 @@
     update.commit();
 
     ChangeNotes notes = newNotes(c);
-    Timestamp ts = new Timestamp(update.getWhen().getTime());
+    Instant ts = update.getWhen();
     assertThat(notes.getReviewers())
         .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(REVIEWER, Account.id(2), ts)));
 
@@ -1143,7 +1142,7 @@
     update.commit();
 
     notes = newNotes(c);
-    ts = new Timestamp(update.getWhen().getTime());
+    ts = update.getWhen();
     assertThat(notes.getReviewers())
         .isEqualTo(ReviewerSet.fromTable(ImmutableTable.of(CC, Account.id(2), ts)));
   }
@@ -1278,7 +1277,7 @@
     update.commit();
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getMergedOn()).isPresent();
-    Timestamp mergedOn = notes.getMergedOn().get();
+    Instant mergedOn = notes.getMergedOn().get();
     assertThat(mergedOn).isEqualTo(notes.getChange().getLastUpdatedOn());
 
     // Next update does not change mergedOn date.
@@ -1304,7 +1303,7 @@
     update.commit();
     ChangeNotes notes = newNotes(c);
     assertThat(notes.getMergedOn()).isPresent();
-    Timestamp mergedOn = notes.getMergedOn().get();
+    Instant mergedOn = notes.getMergedOn().get();
     assertThat(mergedOn).isEqualTo(notes.getChange().getLastUpdatedOn());
 
     incrementPatchSet(c);
@@ -1698,7 +1697,7 @@
   public void createdOnChangeNotes() throws Exception {
     Change c = newChange();
 
-    Timestamp createdOn = newNotes(c).getChange().getCreatedOn();
+    Instant createdOn = newNotes(c).getChange().getCreatedOn();
     assertThat(createdOn).isNotNull();
 
     // An update doesn't affect the createdOn timestamp.
@@ -1713,54 +1712,54 @@
     Change c = newChange();
 
     ChangeNotes notes = newNotes(c);
-    Timestamp ts1 = notes.getChange().getLastUpdatedOn();
+    Instant ts1 = notes.getChange().getLastUpdatedOn();
     assertThat(ts1).isEqualTo(notes.getChange().getCreatedOn());
 
     // Various kinds of updates that update the timestamp.
     ChangeUpdate update = newUpdate(c, changeOwner);
     update.setTopic("topic"); // Change something to get a new commit.
     update.commit();
-    Timestamp ts2 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts2 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts2).isGreaterThan(ts1);
 
     update = newUpdate(c, changeOwner);
     update.setChangeMessage("Some message");
     update.commit();
-    Timestamp ts3 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts3 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts3).isGreaterThan(ts2);
 
     update = newUpdate(c, changeOwner);
     update.setHashtags(ImmutableSet.of("foo"));
     update.commit();
-    Timestamp ts4 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts4 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts4).isGreaterThan(ts3);
 
     incrementPatchSet(c);
-    Timestamp ts5 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts5 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts5).isGreaterThan(ts4);
 
     update = newUpdate(c, changeOwner);
     update.putApproval(LabelId.CODE_REVIEW, (short) 1);
     update.commit();
-    Timestamp ts6 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts6 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts6).isGreaterThan(ts5);
 
     update = newUpdate(c, changeOwner);
     update.setStatus(Change.Status.ABANDONED);
     update.commit();
-    Timestamp ts7 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts7 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts7).isGreaterThan(ts6);
 
     update = newUpdate(c, changeOwner);
     update.putReviewer(otherUser.getAccountId(), ReviewerStateInternal.REVIEWER);
     update.commit();
-    Timestamp ts8 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts8 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts8).isGreaterThan(ts7);
 
     update = newUpdate(c, changeOwner);
     update.setGroups(ImmutableList.of("a", "b"));
     update.commit();
-    Timestamp ts9 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts9 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts9).isGreaterThan(ts8);
 
     // Finish off by merging the change.
@@ -1774,7 +1773,7 @@
                 submitLabel(LabelId.VERIFIED, "OK", changeOwner.getAccountId()),
                 submitLabel("Alternative-Code-Review", "NEED", null))));
     update.commit();
-    Timestamp ts10 = newNotes(c).getChange().getLastUpdatedOn();
+    Instant ts10 = newNotes(c).getChange().getLastUpdatedOn();
     assertThat(ts10).isGreaterThan(ts9);
   }
 
@@ -1887,7 +1886,7 @@
             1,
             changeOwner,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "Comment",
             (short) 1,
             commit,
@@ -1976,7 +1975,7 @@
     // comment on ps2
     update = newUpdate(c, changeOwner);
     update.setPatchSetId(psId2);
-    Timestamp ts = TimeUtil.nowTs();
+    Instant ts = TimeUtil.now();
     update.putComment(
         HumanComment.Status.PUBLISHED,
         newComment(
@@ -2044,7 +2043,7 @@
     String uuid1 = "uuid1";
     String message1 = "comment 1";
     CommentRange range1 = new CommentRange(1, 1, 2, 1);
-    Timestamp time1 = TimeUtil.nowTs();
+    Instant time1 = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
     RevCommit tipCommit;
     try (NoteDbUpdateManager updateManager = updateManagerFactory.create(project)) {
@@ -2293,7 +2292,7 @@
             0,
             otherUser,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "message",
             (short) 1,
             commitId,
@@ -2323,7 +2322,7 @@
             range.getEndLine(),
             otherUser,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "message",
             (short) 1,
             commitId,
@@ -2353,7 +2352,7 @@
             range.getEndLine(),
             otherUser,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "message",
             (short) 1,
             commitId,
@@ -2383,7 +2382,7 @@
             range.getEndLine(),
             otherUser,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "message",
             (short) 1,
             commitId,
@@ -2410,7 +2409,7 @@
     String message3 = "comment 3";
     CommentRange range1 = new CommentRange(1, 1, 2, 1);
     CommentRange range2 = new CommentRange(2, 1, 3, 1);
-    Timestamp time = TimeUtil.nowTs();
+    Instant time = TimeUtil.now();
     ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
     HumanComment comment1 =
@@ -2480,7 +2479,7 @@
     String uuid = "uuid";
     String message = "comment";
     CommentRange range = new CommentRange(1, 1, 2, 1);
-    Timestamp time = TimeUtil.nowTs();
+    Instant time = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
     ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
 
@@ -2510,7 +2509,7 @@
 
   @Test
   public void patchLineCommentNotesFormatWeirdUser() throws Exception {
-    Account.Builder account = Account.builder(Account.id(3), TimeUtil.nowTs());
+    Account.Builder account = Account.builder(Account.id(3), TimeUtil.now());
     account.setFullName("Weird\n\u0002<User>\n");
     account.setPreferredEmail(" we\r\nird@ex>ample<.com");
     accountCache.put(account.build());
@@ -2520,7 +2519,7 @@
     ChangeUpdate update = newUpdate(c, user);
     String uuid = "uuid";
     CommentRange range = new CommentRange(1, 1, 2, 1);
-    Timestamp time = TimeUtil.nowTs();
+    Instant time = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     HumanComment comment =
@@ -2558,7 +2557,7 @@
     String messageForBase = "comment for base";
     String messageForPS = "comment for ps";
     CommentRange range = new CommentRange(1, 1, 2, 1);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     HumanComment commentForBase =
@@ -2617,8 +2616,8 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp timeForComment1 = TimeUtil.nowTs();
-    Timestamp timeForComment2 = TimeUtil.nowTs();
+    Instant timeForComment1 = TimeUtil.now();
+    Instant timeForComment2 = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             psId,
@@ -2676,7 +2675,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             psId,
@@ -2734,7 +2733,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -2757,7 +2756,7 @@
     PatchSet.Id ps2 = c.currentPatchSetId();
 
     update = newUpdate(c, otherUser);
-    now = TimeUtil.nowTs();
+    now = TimeUtil.now();
     HumanComment comment2 =
         newComment(
             ps2,
@@ -2794,7 +2793,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -2839,7 +2838,7 @@
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
     short side = (short) 1;
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts on the same side of one patch set.
@@ -2909,7 +2908,7 @@
     CommentRange range1 = new CommentRange(1, 1, 2, 2);
     CommentRange range2 = new CommentRange(2, 2, 3, 3);
     String filename = "filename1";
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     // Write two drafts, one on each side of the patchset.
@@ -2984,7 +2983,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment =
         newComment(
             psId,
@@ -3029,7 +3028,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -3052,7 +3051,7 @@
     PatchSet.Id ps2 = c.currentPatchSetId();
 
     update = newUpdate(c, otherUser);
-    now = TimeUtil.nowTs();
+    now = TimeUtil.now();
     HumanComment comment2 =
         newComment(
             ps2,
@@ -3097,7 +3096,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment =
         newComment(
             ps1,
@@ -3130,7 +3129,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment draft =
         newComment(
             ps1,
@@ -3180,7 +3179,7 @@
     String uuid = "uuid";
     ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     String messageForBase = "comment for base";
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     HumanComment comment =
@@ -3212,7 +3211,7 @@
     String uuid = "uuid";
     ObjectId commitId = ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234");
     String messageForBase = "comment for base";
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     PatchSet.Id psId = c.currentPatchSetId();
 
     HumanComment comment =
@@ -3253,7 +3252,7 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(ps2);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -3311,7 +3310,7 @@
 
     ChangeUpdate update = newUpdate(c, otherUser);
     update.setPatchSetId(ps1);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -3385,7 +3384,7 @@
     short side = (short) 1;
 
     ChangeUpdate update = newUpdate(c, otherUser);
-    Timestamp now = TimeUtil.nowTs();
+    Instant now = TimeUtil.now();
     HumanComment comment1 =
         newComment(
             ps1,
@@ -3472,7 +3471,7 @@
             range.getEndLine(),
             otherUser,
             null,
-            new Timestamp(update1.getWhen().getTime()),
+            update1.getWhen(),
             "comment 1",
             (short) 1,
             commitId,
@@ -3489,7 +3488,7 @@
             range.getEndLine(),
             otherUser,
             null,
-            new Timestamp(update2.getWhen().getTime()),
+            update2.getWhen(),
             "comment 2",
             (short) 1,
             commitId,
@@ -3546,7 +3545,7 @@
             range.getEndLine(),
             changeOwner,
             null,
-            new Timestamp(update.getWhen().getTime()),
+            update.getWhen(),
             "comment",
             (short) 1,
             ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
@@ -3982,9 +3981,9 @@
   }
 
   private AttentionSetUpdate addTimestamp(AttentionSetUpdate attentionSetUpdate, Change c) {
-    Timestamp timestamp = newNotes(c).getChange().getLastUpdatedOn();
+    Instant timestamp = newNotes(c).getChange().getLastUpdatedOn();
     return AttentionSetUpdate.createFromRead(
-        timestamp.toInstant(),
+        timestamp,
         attentionSetUpdate.account(),
         attentionSetUpdate.operation(),
         attentionSetUpdate.reason());
@@ -4016,7 +4015,7 @@
             .key(originalPsa.key())
             .value(originalPsa.value())
             .copied(true)
-            .granted(TimeUtil.nowTs())
+            .granted(TimeUtil.now())
             .tag(originalPsa.tag())
             .uuid(originalPsa.uuid())
             .realAccountId(originalPsa.realAccountId())
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
index fa05adc..cf1b5ae 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeUpdateTest.java
@@ -81,7 +81,7 @@
             1,
             changeOwner,
             null,
-            TimeUtil.nowTs(),
+            TimeUtil.now(),
             "Comment",
             (short) 1,
             commit,
diff --git a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
index 2c1348c..02a1cc8 100644
--- a/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommentTimestampAdapterTest.java
@@ -155,7 +155,7 @@
         new HumanComment(
             new Comment.Key("uuid", "filename", 1),
             Account.id(100),
-            NON_DST_TS,
+            NON_DST_TS.toInstant(),
             (short) 0,
             "message",
             "serverId",
diff --git a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
index ce4ec39..5e2e1f2 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitMessageOutputTest.java
@@ -35,6 +35,9 @@
 import org.junit.Test;
 
 public class CommitMessageOutputTest extends AbstractChangeNotesTest {
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void approvalsCommitFormatSimple() throws Exception {
     Change c = TestChanges.newChange(project, changeOwner.getAccountId(), 1);
@@ -67,7 +70,7 @@
     PersonIdent author = commit.getAuthorIdent();
     assertThat(author.getName()).isEqualTo("Gerrit User 1");
     assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
-    assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().getTime() + 1000);
+    assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 1000);
     assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
@@ -142,6 +145,9 @@
   }
 
   @Test
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   public void submitCommitFormat() throws Exception {
     Change c = newChange();
     ChangeUpdate update = newUpdate(c, changeOwner);
@@ -183,7 +189,7 @@
     PersonIdent author = commit.getAuthorIdent();
     assertThat(author.getName()).isEqualTo("Gerrit User 1");
     assertThat(author.getEmailAddress()).isEqualTo("1@gerrit");
-    assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().getTime() + 2000);
+    assertThat(author.getWhen().getTime()).isEqualTo(c.getCreatedOn().toEpochMilli() + 2000);
     assertThat(author.getTimeZone()).isEqualTo(TimeZone.getTimeZone("GMT-7:00"));
 
     PersonIdent committer = commit.getCommitterIdent();
@@ -196,7 +202,7 @@
   @Test
   public void anonymousUser() throws Exception {
     Account anon =
-        Account.builder(Account.id(3), TimeUtil.nowTs())
+        Account.builder(Account.id(3), TimeUtil.now())
             .setMetaId("1234567812345678123456781234567812345678")
             .build();
     accountCache.put(anon);
diff --git a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
index b29ce29..3b18183 100644
--- a/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
+++ b/javatests/com/google/gerrit/server/notedb/CommitRewriterTest.java
@@ -47,8 +47,9 @@
 import com.google.gerrit.server.util.time.TimeUtil;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -320,15 +321,18 @@
     assertThat(secondRunResult.fixedRefDiff.keySet().size()).isEqualTo(expectedSecondRunResult);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixAuthorIdent() throws Exception {
     Change c = newChange();
-    Timestamp when = TimeUtil.nowTs();
+    Instant when = TimeUtil.now();
     PersonIdent invalidAuthorIdent =
         new PersonIdent(
             changeOwner.getName(),
             changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
-            when,
+            Date.from(when),
             serverIdent.getTimeZone());
     RevCommit invalidUpdateCommit =
         writeUpdate(
@@ -449,6 +453,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixReviewerFooterIdent() throws Exception {
     Change c = newChange();
@@ -495,7 +502,7 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+    Instant updateTimestamp = serverIdent.getWhen().toInstant();
     ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
         ImmutableList.of(
             ReviewerStatusUpdate.create(
@@ -532,6 +539,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixReviewerMessage() throws Exception {
     Change c = newChange();
@@ -579,21 +589,15 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+    Instant updateTimestamp = serverIdent.getWhen().toInstant();
     ImmutableList<ReviewerStatusUpdate> expectedReviewerUpdates =
         ImmutableList.of(
             ReviewerStatusUpdate.create(
-                new Timestamp(addReviewerUpdate.when.getTime()),
-                changeOwner.getAccountId(),
-                otherUserId,
-                REVIEWER),
+                addReviewerUpdate.when, changeOwner.getAccountId(), otherUserId, REVIEWER),
             ReviewerStatusUpdate.create(
                 updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED),
             ReviewerStatusUpdate.create(
-                new Timestamp(addCcUpdate.when.getTime()),
-                changeOwner.getAccountId(),
-                otherUserId,
-                CC),
+                addCcUpdate.when, changeOwner.getAccountId(), otherUserId, CC),
             ReviewerStatusUpdate.create(
                 updateTimestamp, changeOwner.getAccountId(), otherUserId, REMOVED));
     ChangeNotes notesAfterRewrite = newNotes(c);
@@ -665,6 +669,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixLabelFooterIdent() throws Exception {
     Change c = newChange();
@@ -715,7 +722,7 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+    Instant updateTimestamp = serverIdent.getWhen().toInstant();
     ImmutableList<PatchSetApproval> expectedApprovals =
         ImmutableList.of(
             PatchSetApproval.builder()
@@ -799,6 +806,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixRemoveVoteChangeMessage() throws Exception {
     Change c = newChange();
@@ -852,7 +862,7 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
 
-    Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+    Instant updateTimestamp = serverIdent.getWhen().toInstant();
     ImmutableList<PatchSetApproval> expectedApprovals =
         ImmutableList.of(
             PatchSetApproval.builder()
@@ -922,6 +932,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixRemoveVoteChangeMessageWithUnparsableAuthorIdent() throws Exception {
     Change c = newChange();
@@ -929,7 +942,7 @@
         new PersonIdent(
             changeOwner.getName(),
             "server@" + serverId,
-            TimeUtil.nowTs(),
+            Date.from(TimeUtil.now()),
             serverIdent.getTimeZone());
     writeUpdate(
         RefNames.changeMetaRef(c.getId()),
@@ -1065,7 +1078,7 @@
   public void fixRemoveVoteChangeMessageWithNoFooterLabel_matchDuplicateAccounts()
       throws Exception {
     Account duplicateCodeOwner =
-        Account.builder(Account.id(4), TimeUtil.nowTs())
+        Account.builder(Account.id(4), TimeUtil.now())
             .setFullName(changeOwner.getName())
             .setPreferredEmail("other@test.com")
             .build();
@@ -1175,6 +1188,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixAttentionFooter() throws Exception {
     Change c = newChange();
@@ -1255,46 +1271,46 @@
     BackfillResult result = rewriter.backfillProject(project, repo, options);
     assertThat(result.fixedRefDiff.keySet()).containsExactly(RefNames.changeMetaRef(c.getId()));
     notesBeforeRewrite.getAttentionSetUpdates();
-    Timestamp updateTimestamp = new Timestamp(serverIdent.getWhen().getTime());
+    Instant updateTimestamp = serverIdent.getWhen().toInstant();
     ImmutableList<AttentionSetUpdate> attentionSetUpdatesBeforeRewrite =
         ImmutableList.of(
             AttentionSetUpdate.createFromRead(
-                invalidRemovedByClickUpdate.getWhen().toInstant(),
+                invalidRemovedByClickUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.REMOVE,
                 String.format("Removed by %s by clicking the attention icon", otherUser.getName())),
             AttentionSetUpdate.createFromRead(
-                validAttentionSetUpdate.getWhen().toInstant(),
+                validAttentionSetUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.ADD,
                 "Added by someone"),
             AttentionSetUpdate.createFromRead(
-                validAttentionSetUpdate.getWhen().toInstant(),
+                validAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.REMOVE,
                 "Removed by someone"),
             AttentionSetUpdate.createFromRead(
-                updateTimestamp.toInstant(),
+                updateTimestamp,
                 changeOwner.getAccountId(),
                 Operation.REMOVE,
                 String.format("%s replied on the change", otherUser.getName())),
             AttentionSetUpdate.createFromRead(
-                updateTimestamp.toInstant(),
+                updateTimestamp,
                 otherUserId,
                 Operation.ADD,
                 "Added by someone using the hovercard menu"),
             AttentionSetUpdate.createFromRead(
-                invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+                invalidMultipleAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.REMOVE,
                 String.format("Removed by %s using the hovercard menu", otherUser.getName())),
             AttentionSetUpdate.createFromRead(
-                invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+                invalidMultipleAttentionSetUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.ADD,
                 String.format("%s replied on the change", otherUser.getName())),
             AttentionSetUpdate.createFromRead(
-                invalidAttentionSetUpdate.getWhen().toInstant(),
+                invalidAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.ADD,
                 String.format("Added by %s using the hovercard menu", otherUser.getName())));
@@ -1302,42 +1318,42 @@
     ImmutableList<AttentionSetUpdate> attentionSetUpdatesAfterRewrite =
         ImmutableList.of(
             AttentionSetUpdate.createFromRead(
-                invalidRemovedByClickUpdate.getWhen().toInstant(),
+                invalidRemovedByClickUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.REMOVE,
                 "Removed by someone by clicking the attention icon"),
             AttentionSetUpdate.createFromRead(
-                validAttentionSetUpdate.getWhen().toInstant(),
+                validAttentionSetUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.ADD,
                 "Added by someone"),
             AttentionSetUpdate.createFromRead(
-                validAttentionSetUpdate.getWhen().toInstant(),
+                validAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.REMOVE,
                 "Removed by someone"),
             AttentionSetUpdate.createFromRead(
-                updateTimestamp.toInstant(),
+                updateTimestamp,
                 changeOwner.getAccountId(),
                 Operation.REMOVE,
                 "Someone replied on the change"),
             AttentionSetUpdate.createFromRead(
-                updateTimestamp.toInstant(),
+                updateTimestamp,
                 otherUserId,
                 Operation.ADD,
                 "Added by someone using the hovercard menu"),
             AttentionSetUpdate.createFromRead(
-                invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+                invalidMultipleAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.REMOVE,
                 "Removed by someone using the hovercard menu"),
             AttentionSetUpdate.createFromRead(
-                invalidMultipleAttentionSetUpdate.getWhen().toInstant(),
+                invalidMultipleAttentionSetUpdate.getWhen(),
                 changeOwner.getAccountId(),
                 Operation.ADD,
                 "Someone replied on the change"),
             AttentionSetUpdate.createFromRead(
-                invalidAttentionSetUpdate.getWhen().toInstant(),
+                invalidAttentionSetUpdate.getWhen(),
                 otherUserId,
                 Operation.ADD,
                 "Added by someone using the hovercard menu"));
@@ -1432,22 +1448,22 @@
       thirdAttentionSetUpdate.commit();
       attentionSetUpdatesBeforeRewrite.add(
           AttentionSetUpdate.createFromRead(
-              thirdAttentionSetUpdate.getWhen().toInstant(),
+              thirdAttentionSetUpdate.getWhen(),
               changeOwner.getAccountId(),
               Operation.REMOVE,
               String.format("Removed by %s by clicking the attention icon", okAccountName)),
           AttentionSetUpdate.createFromRead(
-              secondAttentionSetUpdate.getWhen().toInstant(),
+              secondAttentionSetUpdate.getWhen(),
               otherUserId,
               Operation.REMOVE,
               String.format("Removed by %s using the hovercard menu", okAccountName)),
           AttentionSetUpdate.createFromRead(
-              secondAttentionSetUpdate.getWhen().toInstant(),
+              secondAttentionSetUpdate.getWhen(),
               changeOwner.getAccountId(),
               Operation.ADD,
               String.format("%s replied on the change", okAccountName)),
           AttentionSetUpdate.createFromRead(
-              firstAttentionSetUpdate.getWhen().toInstant(),
+              firstAttentionSetUpdate.getWhen(),
               otherUserId,
               Operation.ADD,
               String.format("Added by %s using the hovercard menu", okAccountName)));
@@ -1553,6 +1569,9 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void fixSubmitChangeMessageAndFooters() throws Exception {
     Change c = newChange();
@@ -1560,7 +1579,7 @@
         new PersonIdent(
             changeOwner.getName(),
             changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
-            TimeUtil.nowTs(),
+            Date.from(TimeUtil.now()),
             serverIdent.getTimeZone());
     String changeOwnerIdentToFix = getAccountIdentToFix(changeOwner.getAccount());
     writeUpdate(
@@ -1756,16 +1775,16 @@
   public void fixCodeOwnersOnAddReviewerChangeMessage() throws Exception {
 
     Account reviewer =
-        Account.builder(Account.id(3), TimeUtil.nowTs())
+        Account.builder(Account.id(3), TimeUtil.now())
             .setFullName("Reviewer User")
             .setPreferredEmail("reviewer@account.com")
             .build();
     accountCache.put(reviewer);
     Account duplicateCodeOwner =
-        Account.builder(Account.id(4), TimeUtil.nowTs()).setFullName(changeOwner.getName()).build();
+        Account.builder(Account.id(4), TimeUtil.now()).setFullName(changeOwner.getName()).build();
     accountCache.put(duplicateCodeOwner);
     Account duplicateReviewer =
-        Account.builder(Account.id(5), TimeUtil.nowTs()).setFullName(reviewer.getName()).build();
+        Account.builder(Account.id(5), TimeUtil.now()).setFullName(reviewer.getName()).build();
     accountCache.put(duplicateReviewer);
     Change c = newChange();
     ImmutableList.Builder<ObjectId> commitsToFix = new ImmutableList.Builder<>();
@@ -2223,7 +2242,7 @@
         getChangeUpdateBody(c, "Assignee deleted: " + otherUser.getName()),
         getAuthorIdent(changeOwner.getAccount()));
     Account reviewer =
-        Account.builder(Account.id(3), TimeUtil.nowTs())
+        Account.builder(Account.id(3), TimeUtil.now())
             .setFullName("Reviewer User")
             .setPreferredEmail("reviewer@account.com")
             .build();
@@ -2262,16 +2281,19 @@
     assertThat(secondRunResult.refsFailedToFix).isEmpty();
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void singleRunFixesAll() throws Exception {
     Change c = newChange();
-    Timestamp when = TimeUtil.nowTs();
+    Instant when = TimeUtil.now();
     String assigneeIdentToFix = getAccountIdentToFix(otherUser.getAccount());
     PersonIdent authorIdentToFix =
         new PersonIdent(
             changeOwner.getName(),
             changeNoteUtil.getAccountIdAsEmailAddress(changeOwner.getAccountId()),
-            when,
+            Date.from(when),
             serverIdent.getTimeZone());
 
     RevCommit invalidUpdateCommit =
@@ -2428,7 +2450,6 @@
   }
 
   private PersonIdent getAuthorIdent(Account account) {
-    Timestamp when = TimeUtil.nowTs();
-    return changeNoteUtil.newAccountIdIdent(account.id(), when, serverIdent);
+    return changeNoteUtil.newAccountIdIdent(account.id(), TimeUtil.now(), serverIdent);
   }
 }
diff --git a/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
index 041366c..31b1db0 100644
--- a/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/DraftCommentNotesTest.java
@@ -89,7 +89,7 @@
         0,
         otherUser,
         null,
-        TimeUtil.nowTs(),
+        TimeUtil.now(),
         "comment",
         (short) 0,
         ObjectId.fromString("abcd1234abcd1234abcd1234abcd1234abcd1234"),
diff --git a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
index 5a8b266..9f0fc29 100644
--- a/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
+++ b/javatests/com/google/gerrit/server/patch/DiffOperationsTest.java
@@ -32,6 +32,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import java.io.IOException;
+import java.util.Date;
 import java.util.Map;
 import java.util.Optional;
 import org.eclipse.jgit.lib.CommitBuilder;
@@ -256,11 +257,14 @@
         : createCommitInRepo(repo, treeId, parentCommit);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   private static ObjectId createCommitInRepo(Repository repo, ObjectId treeId, ObjectId... parents)
       throws IOException {
     try (ObjectInserter oi = repo.newObjectInserter()) {
       PersonIdent committer =
-          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), TimeUtil.nowTs());
+          new PersonIdent(new PersonIdent("Foo Bar", "foo.bar@baz.com"), Date.from(TimeUtil.now()));
       CommitBuilder cb = new CommitBuilder();
       cb.setTreeId(treeId);
       cb.setCommitter(committer);
diff --git a/javatests/com/google/gerrit/server/patch/MagicFileTest.java b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
index 93928f0..21ea641 100644
--- a/javatests/com/google/gerrit/server/patch/MagicFileTest.java
+++ b/javatests/com/google/gerrit/server/patch/MagicFileTest.java
@@ -95,6 +95,9 @@
     assertThat(magicFile.getStartLineOfModifiableContent()).isEqualTo(1);
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void commitMessageFileOfRootCommitContainsCorrectContent() throws Exception {
     try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
@@ -146,6 +149,9 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void commitMessageFileOfNonMergeCommitContainsCorrectContent() throws Exception {
     try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
@@ -202,6 +208,9 @@
     }
   }
 
+  // TODO(issue-15517): Fix the JdkObsolete issue with Date once JGit's PersonIdent class supports
+  // Instants
+  @SuppressWarnings("JdkObsolete")
   @Test
   public void commitMessageFileOfMergeCommitContainsCorrectContent() throws Exception {
     try (Repository repository = repositoryManager.createRepository(Project.nameKey("repo1"));
diff --git a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
index ecdb066..55340e3 100644
--- a/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
+++ b/javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
@@ -142,6 +142,7 @@
 import com.google.inject.Provider;
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -923,6 +924,7 @@
 
   @Test
   public void byTopic() throws Exception {
+
     TestRepository<Repo> repo = createProject("repo");
     ChangeInserter ins1 = newChangeWithTopic(repo, "feature1");
     Change change1 = insert(repo, ins1);
@@ -953,6 +955,11 @@
     assertQuery("intopic:gerrit", change6, change5);
     assertQuery("topic:\"\"", change_no_topic);
     assertQuery("intopic:\"\"", change_no_topic);
+
+    assume().that(getSchema().hasField(ChangeField.PREFIX_TOPIC)).isTrue();
+    assertQuery("prefixtopic:feature", change4, change2, change1);
+    assertQuery("prefixtopic:Cher", change3);
+    assertQuery("prefixtopic:feature22");
   }
 
   @Test
@@ -1775,8 +1782,9 @@
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
     long startMs = TestTimeUtil.START.toEpochMilli();
-    Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
-    Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+    Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+    Change change2 =
+        insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
 
     // Stop time so age queries use the same endpoint.
     TestTimeUtil.setClockStep(0, MILLISECONDS);
@@ -1815,8 +1823,9 @@
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
     long startMs = TestTimeUtil.START.toEpochMilli();
-    Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
-    Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+    Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+    Change change2 =
+        insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
     TestTimeUtil.setClockStep(0, MILLISECONDS);
 
     // Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -1866,8 +1875,9 @@
     resetTimeWithClockStep(thirtyHoursInMs, MILLISECONDS);
     TestRepository<Repo> repo = createProject("repo");
     long startMs = TestTimeUtil.START.toEpochMilli();
-    Change change1 = insert(repo, newChange(repo), null, new Timestamp(startMs));
-    Change change2 = insert(repo, newChange(repo), null, new Timestamp(startMs + thirtyHoursInMs));
+    Change change1 = insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs));
+    Change change2 =
+        insert(repo, newChange(repo), null, Instant.ofEpochMilli(startMs + thirtyHoursInMs));
     TestTimeUtil.setClockStep(0, MILLISECONDS);
 
     // Change1 was last updated on 2009-09-30 21:00:00 -0000
@@ -2177,6 +2187,15 @@
   }
 
   @Test
+  public void byHashtagPrefix() throws Exception {
+    assume().that(getSchema().hasField(ChangeField.PREFIX_HASHTAG)).isTrue();
+    List<Change> changes = setUpHashtagChanges();
+    assertQuery("prefixhashtag:a", changes.get(1), changes.get(0));
+    assertQuery("prefixhashtag:aa", changes.get(0));
+    assertQuery("prefixhashtag:bar", changes.get(1));
+  }
+
+  @Test
   public void byHashtagRegex() throws Exception {
     TestRepository<Repo> repo = createProject("repo");
     Change change1 = insert(repo, newChange(repo));
@@ -4134,19 +4153,16 @@
   }
 
   protected Change insert(TestRepository<Repo> repo, ChangeInserter ins) throws Exception {
-    return insert(repo, ins, null, TimeUtil.nowTs());
+    return insert(repo, ins, null, TimeUtil.now());
   }
 
   protected Change insert(TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner)
       throws Exception {
-    return insert(repo, ins, owner, TimeUtil.nowTs());
+    return insert(repo, ins, owner, TimeUtil.now());
   }
 
   protected Change insert(
-      TestRepository<Repo> repo,
-      ChangeInserter ins,
-      @Nullable Account.Id owner,
-      Timestamp createdOn)
+      TestRepository<Repo> repo, ChangeInserter ins, @Nullable Account.Id owner, Instant createdOn)
       throws Exception {
     Project.NameKey project =
         Project.nameKey(repo.getRepository().getDescription().getRepositoryName());
@@ -4172,7 +4188,7 @@
             .create(changeNotesFactory.createChecked(c), PatchSet.id(c.getId(), n), commit)
             .setFireRevisionCreated(false)
             .setValidate(false);
-    try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.nowTs());
+    try (BatchUpdate bu = updateFactory.create(c.getProject(), user, TimeUtil.now());
         ObjectInserter oi = repo.getRepository().newObjectInserter();
         ObjectReader reader = oi.newReader();
         RevWalk rw = new RevWalk(reader)) {
@@ -4311,7 +4327,7 @@
   }
 
   protected static long lastUpdatedMs(Change c) {
-    return c.getLastUpdatedOn().getTime();
+    return c.getLastUpdatedOn().toEpochMilli();
   }
 
   // Get the last  updated time from ChangeApi
diff --git a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
index e42230f..e48d4af 100644
--- a/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
+++ b/javatests/com/google/gerrit/server/query/change/ChangeDataTest.java
@@ -46,7 +46,7 @@
         .id(PatchSet.id(changeId, num))
         .commitId(ObjectId.zeroId())
         .uploader(Account.id(1234))
-        .createdOn(TimeUtil.nowTs())
+        .createdOn(TimeUtil.now())
         .build();
   }
 }
diff --git a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
index 47bfb2a..4c8750a 100644
--- a/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/CommentPorterTest.java
@@ -41,7 +41,7 @@
 import com.google.gerrit.server.patch.DiffOptions;
 import com.google.gerrit.server.restapi.change.CommentPorter.Metrics;
 import com.google.gerrit.truth.NullAwareCorrespondence;
-import java.sql.Timestamp;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Optional;
 import org.eclipse.jgit.lib.ObjectId;
@@ -231,7 +231,7 @@
         changeId,
         Account.id(123),
         BranchNameKey.create(project, "myBranch"),
-        new Timestamp(12345));
+        Instant.ofEpochMilli(12345));
   }
 
   private PatchSet createPatchset(PatchSet.Id id) {
@@ -239,7 +239,7 @@
         .id(id)
         .commitId(dummyObjectId)
         .uploader(Account.id(123))
-        .createdOn(new Timestamp(12345))
+        .createdOn(Instant.ofEpochMilli(12345))
         .build();
   }
 
@@ -262,7 +262,7 @@
     return new HumanComment(
         new Comment.Key(getUniqueUuid(), filePath, patchsetId.get()),
         Account.id(100),
-        new Timestamp(1234),
+        Instant.ofEpochMilli(1234),
         (short) 1,
         "Comment text",
         "serverId",
diff --git a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
index 44c3cef..2685a8b 100644
--- a/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
+++ b/javatests/com/google/gerrit/server/restapi/change/ListChangeCommentsTest.java
@@ -23,6 +23,9 @@
 import com.google.gerrit.server.ChangeMessagesUtil;
 import com.google.gerrit.server.CommentsUtil;
 import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
@@ -124,9 +127,11 @@
   /** Create a new change message with an id, message, timestamp and tag */
   private static ChangeMessage newChangeMessage(String id, String message, String ts, String tag) {
     ChangeMessage.Key key = ChangeMessage.key(Change.id(1), id);
-    ChangeMessage cm =
-        ChangeMessage.create(
-            key, null, Timestamp.valueOf("2000-01-01 00:00:" + ts), null, message, null, tag);
+    Instant timestamp =
+        DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+            .withZone(ZoneId.systemDefault())
+            .parse("2000-01-01 00:00:" + ts, Instant::from);
+    ChangeMessage cm = ChangeMessage.create(key, null, timestamp, null, message, null, tag);
     return cm;
   }
 
diff --git a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
index e5dd817..509447a 100644
--- a/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
+++ b/javatests/com/google/gerrit/server/rules/IgnoreSelfApprovalRuleTest.java
@@ -27,7 +27,6 @@
 import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Date;
 import java.util.List;
 import org.junit.Test;
 
@@ -84,7 +83,7 @@
     return PatchSetApproval.builder()
         .key(PatchSetApproval.key(PS_ID, accountId, labelId))
         .value(value)
-        .granted(Date.from(Instant.now()))
+        .granted(Instant.now())
         .build();
   }
 
diff --git a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
index 10599c6..1f22564 100644
--- a/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
+++ b/javatests/com/google/gerrit/server/update/BatchUpdateTest.java
@@ -95,7 +95,7 @@
     RevCommit masterCommit = repo.branch("master").commit().create();
     RevCommit branchCommit = repo.branch("branch").commit().parent(masterCommit).create();
 
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addRepoOnlyOp(
           new RepoOnlyOp() {
             @Override
@@ -114,7 +114,7 @@
   public void cannotExceedMaxUpdates() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(id, new AddMessageOp("Excessive update"));
       ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute);
       assertThat(thrown)
@@ -130,7 +130,7 @@
     Change.Id id = createChangeWithPatchSets(2);
 
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(id, new AddMessageOp("Update on PS1", PatchSet.id(id, 1)));
       bu.addOp(id, new AddMessageOp("Update on PS2", PatchSet.id(id, 2)));
       ResourceConflictException thrown = assertThrows(ResourceConflictException.class, bu::execute);
@@ -146,7 +146,7 @@
   public void exceedingMaxUpdatesAllowedWithCompleteNoOp() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(
           id,
           new BatchUpdateOp() {
@@ -165,7 +165,7 @@
   public void exceedingMaxUpdatesAllowedWithNoOpAfterPopulatingUpdate() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(
           id,
           new BatchUpdateOp() {
@@ -185,7 +185,7 @@
   public void exceedingMaxUpdatesAllowedWithSubmit() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(id, new SubmitOp());
       bu.execute();
     }
@@ -197,7 +197,7 @@
   public void exceedingMaxUpdatesAllowedWithSubmitAfterOtherOp() throws Exception {
     Change.Id id = createChangeWithPatchSets(2);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(id, new AddMessageOp("Message on PS1", PatchSet.id(id, 1)));
       bu.addOp(id, new SubmitOp());
       bu.execute();
@@ -212,7 +212,7 @@
   public void exceedingMaxUpdatesAllowedWithAbandon() throws Exception {
     Change.Id id = createChangeWithUpdates(MAX_UPDATES);
     ObjectId oldMetaId = getMetaId(id);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.addOp(
           id,
           new BatchUpdateOp() {
@@ -235,7 +235,7 @@
     Change.Id changeId = createChangeWithPatchSets(MAX_PATCH_SETS);
     ObjectId oldMetaId = getMetaId(changeId);
     ChangeNotes notes = changeNotesFactory.create(project, changeId);
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       ObjectId commitId =
           repo.amend(notes.getCurrentPatchSet().commitId()).message("kaboom").create();
       bu.addOp(
@@ -257,7 +257,7 @@
     Change.Id changeId = createChangeWithUpdates(1);
     ChangeNotes notes = changeNotesFactory.create(project, changeId);
 
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       ObjectId commitId =
           repo.amend(notes.getCurrentPatchSet().commitId())
               .add("bar.txt", "bar")
@@ -285,7 +285,7 @@
     int cacheSizeBefore = diffSummaryCache.asMap().size();
 
     // We don't want to depend on the test helper used above so we perform an explicit commit here.
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       ObjectId commitId =
           repo.amend(notes.getCurrentPatchSet().commitId())
               .add("bar.txt", "bar")
@@ -309,7 +309,7 @@
     checkArgument(totalUpdates > 0);
     checkArgument(totalUpdates <= MAX_UPDATES);
     Change.Id id = Change.id(sequences.nextChangeId());
-    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+    try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
       bu.insertChange(
           changeInserterFactory.create(
               id, repo.commit().message("Change").insertChangeId().create(), "refs/heads/master"));
@@ -317,7 +317,7 @@
     }
     assertThat(getUpdateCount(id)).isEqualTo(1);
     for (int i = 2; i <= totalUpdates; i++) {
-      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
         bu.addOp(id, new AddMessageOp("Update " + i));
         bu.execute();
       }
@@ -331,7 +331,7 @@
     Change.Id id = createChangeWithUpdates(MAX_UPDATES - 2);
     ChangeNotes notes = changeNotesFactory.create(project, id);
     for (int i = 2; i <= patchSets; ++i) {
-      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.nowTs())) {
+      try (BatchUpdate bu = batchUpdateFactory.create(project, user.get(), TimeUtil.now())) {
         ObjectId commitId =
             repo.amend(notes.getCurrentPatchSet().commitId()).message("PS" + i).create();
         bu.addOp(
diff --git a/plugins/delete-project b/plugins/delete-project
index fac8815..5717bad 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit fac8815949114d58b65dceda355bf80f7ec2adee
+Subproject commit 5717badf4250dfe900c05fc00d0758a09ba77297
diff --git a/plugins/gitiles b/plugins/gitiles
index fa993c0..97ce60f 160000
--- a/plugins/gitiles
+++ b/plugins/gitiles
@@ -1 +1 @@
-Subproject commit fa993c05a1861c766c79cc304cc2ce4e9ae2905a
+Subproject commit 97ce60f8bb4dbf40dde79cf56db6425c384dabcf
diff --git a/plugins/plugin-manager b/plugins/plugin-manager
index 367f434..dbd6820 160000
--- a/plugins/plugin-manager
+++ b/plugins/plugin-manager
@@ -1 +1 @@
-Subproject commit 367f43435c8c0699828abe24e765e4fc0881737e
+Subproject commit dbd68200d867513e2c0449798476e275aaf08cfd
diff --git a/plugins/replication b/plugins/replication
index 36ed18a..e19028c 160000
--- a/plugins/replication
+++ b/plugins/replication
@@ -1 +1 @@
-Subproject commit 36ed18af69d005a7cf89a9bba2f2585ead8d46da
+Subproject commit e19028c4f7e5d26bbc4be7762aa96434bf1d7781
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
index a28ae59..6226d01 160000
--- a/plugins/reviewnotes
+++ b/plugins/reviewnotes
@@ -1 +1 @@
-Subproject commit a28ae590486934690e4e0a95d7eb75f8b60644a6
+Subproject commit 6226d01c563846ae479cb4fdafd698b31472772c
diff --git a/plugins/webhooks b/plugins/webhooks
index 3b1ca2e..7830135 160000
--- a/plugins/webhooks
+++ b/plugins/webhooks
@@ -1 +1 @@
-Subproject commit 3b1ca2e743ac74c3f1f4181b9b708b8fdf76c0af
+Subproject commit 7830135fd85ff9de10a8ca1f2e6112af59fca15f
diff --git a/polygerrit-ui/app/BUILD b/polygerrit-ui/app/BUILD
index 898de14..80e208d 100644
--- a/polygerrit-ui/app/BUILD
+++ b/polygerrit-ui/app/BUILD
@@ -15,6 +15,7 @@
     "embed",
     "gr-diff",
     "mixins",
+    "models",
     "samples",
     "scripts",
     "services",
@@ -118,7 +119,7 @@
     "elements/gr-app-element_html.ts",
     "elements/settings/gr-watched-projects-editor/gr-watched-projects-editor_html.ts",
     "elements/shared/gr-account-list/gr-account-list_html.ts",
-    "services/dependency.ts",
+    "models/dependency.ts",
 ]
 
 sources_for_template_checking = glob(
diff --git a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
index 88ade26..bed1c54 100644
--- a/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
+++ b/polygerrit-ui/app/elements/admin/gr-create-change-dialog/gr-create-change-dialog.ts
@@ -36,6 +36,8 @@
 import {BindValueChangeEvent} from '../../../types/events';
 import {fireEvent} from '../../../utils/event-util';
 import {subscribe} from '../../lit/subscription-controller';
+import {configModelToken} from '../../../models/config/config-model';
+import {resolve} from '../../../models/dependency';
 
 const SUGGESTIONS_LIMIT = 15;
 const REF_PREFIX = 'refs/heads/';
@@ -76,7 +78,7 @@
 
   private readonly restApiService = getAppContext().restApiService;
 
-  private readonly configModel = getAppContext().configModel;
+  private readonly configModel = resolve(this, configModelToken);
 
   constructor() {
     super();
@@ -87,7 +89,7 @@
     super.connectedCallback();
     if (!this.repoName) return;
 
-    subscribe(this, this.configModel.serverConfig$, config => {
+    subscribe(this, this.configModel().serverConfig$, config => {
       this.privateChangesEnabled =
         config?.change?.disable_private_changes ?? false;
     });
diff --git a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
index 27cdac3..c90359e 100644
--- a/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-summary/gr-change-summary.ts
@@ -59,8 +59,8 @@
 import {modifierPressed} from '../../../utils/dom-util';
 import {DropdownLink} from '../../shared/gr-dropdown/gr-dropdown';
 import {fontStyles} from '../../../styles/gr-font-styles';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve} from '../../../services/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
 
 export enum SummaryChipStyles {
   INFO = 'info',
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index 5a54599..3342f83 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -197,8 +197,8 @@
 } from '../../../utils/attention-set-util';
 import {listen} from '../../../services/shortcuts/shortcuts-service';
 import {LoadingStatus} from '../../../services/change/change-model';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
 
 const MIN_LINES_FOR_COMMIT_COLLAPSE = 18;
 
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
index 9bcedc8..aeff1c7 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-submit-dialog/gr-confirm-submit-dialog.ts
@@ -31,8 +31,8 @@
 import {subscribe} from '../../lit/subscription-controller';
 import {getAppContext} from '../../../services/app-context';
 import {ParsedChangeInfo} from '../../../types/types';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve} from '../../../services/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
 
 @customElement('gr-confirm-submit-dialog')
 export class GrConfirmSubmitDialog extends LitElement {
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
index 94039f4..1918972 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.ts
@@ -85,9 +85,9 @@
 import {RevisionInfo} from '../../shared/revision-info/revision-info';
 import {listen} from '../../../services/shortcuts/shortcuts-service';
 import {select} from '../../../utils/observable-util';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
-import {browserModelToken} from '../../../services/browser/browser-model';
-import {commentsModelToken} from '../../../services/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
 
 export const DEFAULT_NUM_FILES_SHOWN = 200;
 
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index 8ae0115..6b1ecf2 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -50,8 +50,8 @@
   FormattedReviewerUpdateInfo,
   ParsedChangeInfo,
 } from '../../../types/types';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
 
 /**
  * The content of the enum is also used in the UI for the button text.
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index 7097c21..2e95ce3 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -67,6 +67,8 @@
 import {subscribe} from '../lit/subscription-controller';
 import {fontStyles} from '../../styles/gr-font-styles';
 import {fire} from '../../utils/event-util';
+import {resolve} from '../../models/dependency';
+import {configModelToken} from '../../models/config/config-model';
 
 /**
  * Firing this event sets the regular expression of the results filter.
@@ -106,8 +108,8 @@
 
   private checksModel = getAppContext().checksModel;
 
-  constructor() {
-    super();
+  override connectedCallback() {
+    super.connectedCallback();
     subscribe(this, this.changeModel.labels$, x => (this.labels = x));
   }
 
@@ -561,7 +563,7 @@
 
   private changeModel = getAppContext().changeModel;
 
-  private configModel = getAppContext().configModel;
+  private configModel = resolve(this, configModelToken);
 
   static override get styles() {
     return [
@@ -584,9 +586,9 @@
     ];
   }
 
-  constructor() {
-    super();
-    subscribe(this, this.configModel.repoConfig$, x => (this.repoConfig = x));
+  override connectedCallback() {
+    super.connectedCallback();
+    subscribe(this, this.configModel().repoConfig$, x => (this.repoConfig = x));
   }
 
   override render() {
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 21d9b97..5cc3737 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -38,6 +38,8 @@
 import {LitElement, PropertyValues, html, css} from 'lit';
 import {customElement, property, state} from 'lit/decorators';
 import {fireEvent} from '../../../utils/event-util';
+import {resolve} from '../../../models/dependency';
+import {configModelToken} from '../../../models/config/config-model';
 
 type MainHeaderLink = RequireProperties<DropdownLink, 'url' | 'name'>;
 
@@ -153,7 +155,7 @@
 
   private readonly userModel = getAppContext().userModel;
 
-  private readonly configModel = getAppContext().configModel;
+  private readonly configModel = resolve(this, configModelToken);
 
   private subscriptions: Subscription[] = [];
 
@@ -172,7 +174,7 @@
         })
     );
     this.subscriptions.push(
-      this.configModel.serverConfig$.subscribe(config => {
+      this.configModel().serverConfig$.subscribe(config => {
         if (!config) return;
         this.serverConfig = config;
         this.retrieveFeedbackURL(config);
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 9aafd36..4d28f30 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -88,9 +88,9 @@
 import {ChangeComments} from '../gr-comment-api/gr-comment-api';
 import {Subscription} from 'rxjs';
 import {DisplayLine, RenderPreferences} from '../../../api/diff';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
-import {browserModelToken} from '../../../services/browser/browser-model';
-import {commentsModelToken} from '../../../services/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
 
 const EMPTY_BLAME = 'No blame information for this diff.';
 
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
index a5ab586..bfb2bce 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-mode-selector/gr-diff-mode-selector.ts
@@ -26,8 +26,8 @@
 import {FixIronA11yAnnouncer} from '../../../types/types';
 import {getAppContext} from '../../../services/app-context';
 import {fireIronAnnounce} from '../../../utils/event-util';
-import {browserModelToken} from '../../../services/browser/browser-model';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
 
 @customElement('gr-diff-mode-selector')
 export class GrDiffModeSelector extends DIPolymerElement {
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
index 92657dc..459b696 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.ts
@@ -118,9 +118,9 @@
 import {LoadingStatus} from '../../../services/change/change-model';
 import {DisplayLine} from '../../../api/diff';
 import {GrDownloadDialog} from '../../change/gr-download-dialog/gr-download-dialog';
-import {browserModelToken} from '../../../services/browser/browser-model';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve, DIPolymerElement} from '../../../services/dependency';
+import {browserModelToken} from '../../../models/browser/browser-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve, DIPolymerElement} from '../../../models/dependency';
 import {BehaviorSubject} from 'rxjs';
 
 const ERR_REVIEW_STATUS = 'Couldn’t change file review status.';
diff --git a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
index b6397b0..c4ffad4 100644
--- a/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
+++ b/polygerrit-ui/app/elements/diff/gr-patch-range-select/gr-patch-range-select.ts
@@ -51,8 +51,8 @@
 import {LitElement, PropertyValues, css, html} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators';
 import {subscribe} from '../../lit/subscription-controller';
-import {commentsModelToken} from '../../../services/comments/comments-model';
-import {resolve} from '../../../services/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
+import {resolve} from '../../../models/dependency';
 
 // Maximum length for patch set descriptions.
 const PATCH_DESC_MAX_LENGTH = 500;
diff --git a/polygerrit-ui/app/elements/gr-app-element.ts b/polygerrit-ui/app/elements/gr-app-element.ts
index 288489e..efcf8f6 100644
--- a/polygerrit-ui/app/elements/gr-app-element.ts
+++ b/polygerrit-ui/app/elements/gr-app-element.ts
@@ -84,8 +84,8 @@
 import {fireIronAnnounce} from '../utils/event-util';
 import {assertIsDefined} from '../utils/common-util';
 import {listen} from '../services/shortcuts/shortcuts-service';
-import {resolve, DIPolymerElement} from '../services/dependency';
-import {browserModelToken} from '../services/browser/browser-model';
+import {resolve, DIPolymerElement} from '../models/dependency';
+import {browserModelToken} from '../models/browser/browser-model';
 
 interface ErrorInfo {
   text: string;
diff --git a/polygerrit-ui/app/elements/gr-app.ts b/polygerrit-ui/app/elements/gr-app.ts
index ec7379b..1d0b1ad 100644
--- a/polygerrit-ui/app/elements/gr-app.ts
+++ b/polygerrit-ui/app/elements/gr-app.ts
@@ -36,7 +36,7 @@
 import {initGlobalVariables} from './gr-app-global-var-init';
 import './gr-app-element';
 import {Finalizable} from '../services/registry';
-import {provide} from '../services/dependency';
+import {provide} from '../models/dependency';
 import {installPolymerResin} from '../scripts/polymer-resin-install';
 
 import {
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
index f0c9106..bbb743a 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link.ts
@@ -99,6 +99,12 @@
           exportparts="gr-account-label-text: gr-account-link-text"
         >
         </gr-account-label>
+        <gr-endpoint-decorator name="account-status">
+          <gr-endpoint-param
+            name="accountId"
+            .value="${this.account._account_id}"
+          ></gr-endpoint-param>
+        </gr-endpoint-decorator>
       </a>
     </span>`;
   }
diff --git a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
index a78f32f..00990b8 100644
--- a/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-account-link/gr-account-link_test.ts
@@ -45,6 +45,9 @@
           exportparts="gr-account-label-text: gr-account-link-text"
         >
         </gr-account-label>
+        <gr-endpoint-decorator name="account-status">
+          <gr-endpoint-param name="accountId"></gr-endpoint-param>
+        </gr-endpoint-decorator>
       </a>
     </span>
   `);
diff --git a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
index 0f33ffb..b32938a 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment-thread/gr-comment-thread.ts
@@ -70,8 +70,8 @@
 import {ShortcutController} from '../../lit/shortcut-controller';
 import {ValueChangedEvent} from '../../../types/events';
 import {notDeepEqual} from '../../../utils/deep-util';
-import {resolve} from '../../../services/dependency';
-import {commentsModelToken} from '../../../services/comments/comments-model';
+import {resolve} from '../../../models/dependency';
+import {commentsModelToken} from '../../../models/comments/comments-model';
 
 const NEWLINE_PATTERN = /\n/g;
 
@@ -423,7 +423,7 @@
 
   renderFilePath() {
     if (!this.showFilePath) return;
-    const href = this.getUrlForComment();
+    const href = this.getUrlForFileComment();
     const line = this.computeDisplayLine();
     return html`
       ${this.renderFileName()}
@@ -558,7 +558,7 @@
   renderContextualDiff() {
     if (!this.changeNum || !this.showCommentContext || !this.diff) return;
     if (!this.thread?.path) return;
-    const href = this.getUrlForComment();
+    const href = this.getUrlForFileComment();
     return html`
       <div class="diff-container">
         <gr-diff
@@ -704,7 +704,8 @@
     return undefined;
   }
 
-  private getUrlForComment() {
+  // Does not work for patchset level comments
+  private getUrlForFileComment() {
     if (!this.repoName || !this.changeNum || this.isNewThread()) {
       return undefined;
     }
@@ -717,7 +718,17 @@
   }
 
   private handleCopyLink() {
-    const url = this.getUrlForComment();
+    const comment = this.getFirstComment();
+    if (!comment) return;
+    assertIsDefined(this.changeNum, 'changeNum');
+    assertIsDefined(this.repoName, 'repoName');
+    const url = generateAbsoluteUrl(
+      GerritNav.getUrlForCommentsTab(
+        this.changeNum!,
+        this.repoName!,
+        comment.id
+      )
+    );
     assertIsDefined(url, 'url for comment');
     navigator.clipboard.writeText(generateAbsoluteUrl(url)).then(() => {
       fireAlert(this, 'Link copied to clipboard');
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index 18ef5a9..c8dab62 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -30,7 +30,7 @@
 import {getAppContext} from '../../../services/app-context';
 import {css, html, LitElement, PropertyValues} from 'lit';
 import {customElement, property, query, state} from 'lit/decorators';
-import {resolve} from '../../../services/dependency';
+import {resolve} from '../../../models/dependency';
 import {GerritNav} from '../../core/gr-navigation/gr-navigation';
 import {GrTextarea} from '../gr-textarea/gr-textarea';
 import {GrOverlay} from '../gr-overlay/gr-overlay';
@@ -56,7 +56,7 @@
 import {fire, fireEvent} from '../../../utils/event-util';
 import {assertIsDefined} from '../../../utils/common-util';
 import {Key, Modifier} from '../../../utils/dom-util';
-import {commentsModelToken} from '../../../services/comments/comments-model';
+import {commentsModelToken} from '../../../models/comments/comments-model';
 import {sharedStyles} from '../../../styles/shared-styles';
 import {subscribe} from '../../lit/subscription-controller';
 import {ShortcutController} from '../../lit/shortcut-controller';
@@ -66,6 +66,7 @@
 import {getRandomInt} from '../../../utils/math-util';
 import {Subject} from 'rxjs';
 import {debounceTime} from 'rxjs/operators';
+import {configModelToken} from '../../../models/config/config-model';
 
 const UNSAVED_MESSAGE = 'Unable to save draft';
 
@@ -237,7 +238,7 @@
 
   private readonly userModel = getAppContext().userModel;
 
-  private readonly configModel = getAppContext().configModel;
+  private readonly configModel = resolve(this, configModelToken);
 
   private readonly shortcuts = new ShortcutController(this);
 
@@ -263,11 +264,7 @@
     super();
     subscribe(this, this.userModel.account$, x => (this.account = x));
     subscribe(this, this.userModel.isAdmin$, x => (this.isAdmin = x));
-    subscribe(
-      this,
-      this.configModel.repoCommentLinks$,
-      x => (this.commentLinks = x)
-    );
+
     subscribe(this, this.changeModel.repo$, x => (this.repoName = x));
     subscribe(this, this.changeModel.changeNum$, x => (this.changeNum = x));
     subscribe(
@@ -287,6 +284,15 @@
     }
   }
 
+  override connectedCallback() {
+    super.connectedCallback();
+    subscribe(
+      this,
+      this.configModel().repoCommentLinks$,
+      x => (this.commentLinks = x)
+    );
+  }
+
   override disconnectedCallback() {
     // Clean up emoji dropdown.
     if (this.textarea) this.textarea.closeDropdown();
diff --git a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
index 7d31062..bbcfd23 100644
--- a/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
+++ b/polygerrit-ui/app/embed/gr-diff-app-context-init.ts
@@ -87,9 +87,6 @@
     storageService: (_ctx: Partial<AppContext>) => {
       throw new Error('storageService is not implemented');
     },
-    configModel: (_ctx: Partial<AppContext>) => {
-      throw new Error('configModel is not implemented');
-    },
     userModel: (_ctx: Partial<AppContext>) => {
       throw new Error('userModel is not implemented');
     },
diff --git a/polygerrit-ui/app/services/browser/browser-model.ts b/polygerrit-ui/app/models/browser/browser-model.ts
similarity index 83%
rename from polygerrit-ui/app/services/browser/browser-model.ts
rename to polygerrit-ui/app/models/browser/browser-model.ts
index 244ced2..490f868 100644
--- a/polygerrit-ui/app/services/browser/browser-model.ts
+++ b/polygerrit-ui/app/models/browser/browser-model.ts
@@ -14,12 +14,13 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {BehaviorSubject, Observable, combineLatest} from 'rxjs';
+import {Observable, combineLatest} from 'rxjs';
 import {distinctUntilChanged, map} from 'rxjs/operators';
-import {Finalizable} from '../registry';
+import {Finalizable} from '../../services/registry';
 import {define} from '../dependency';
 import {DiffViewMode} from '../../api/diff';
 import {UserModel} from '../user/user-model';
+import {Model} from '../model';
 
 // This value is somewhat arbitrary and not based on research or calculations.
 const MAX_UNIFIED_DEFAULT_WINDOW_WIDTH_PX = 850;
@@ -36,17 +37,12 @@
 
 export const browserModelToken = define<BrowserModel>('browser-model');
 
-export class BrowserModel implements Finalizable {
-  private readonly privateState$ = new BehaviorSubject(initialState);
-
+export class BrowserModel extends Model<BrowserState> implements Finalizable {
   readonly diffViewMode$: Observable<DiffViewMode>;
 
-  get viewState$(): Observable<BrowserState> {
-    return this.privateState$;
-  }
-
   constructor(readonly userModel: UserModel) {
-    const screenWidth$ = this.privateState$.pipe(
+    super(initialState);
+    const screenWidth$ = this.state$.pipe(
       map(
         state =>
           !!state.screenWidth &&
@@ -79,7 +75,7 @@
 
   // Private but used in tests.
   setScreenWidth(screenWidth: number) {
-    this.privateState$.next({...this.privateState$.getValue(), screenWidth});
+    this.subject$.next({...this.subject$.getValue(), screenWidth});
   }
 
   finalize() {}
diff --git a/polygerrit-ui/app/services/comments/comments-model.ts b/polygerrit-ui/app/models/comments/comments-model.ts
similarity index 95%
rename from polygerrit-ui/app/services/comments/comments-model.ts
rename to polygerrit-ui/app/models/comments/comments-model.ts
index 8f47e38..a722e14 100644
--- a/polygerrit-ui/app/services/comments/comments-model.ts
+++ b/polygerrit-ui/app/models/comments/comments-model.ts
@@ -15,7 +15,6 @@
  * limitations under the License.
  */
 
-import {BehaviorSubject} from 'rxjs';
 import {ChangeComments} from '../../elements/diff/gr-comment-api/gr-comment-api';
 import {
   CommentBasics,
@@ -37,19 +36,20 @@
 } from '../../utils/comment-util';
 import {deepEqual} from '../../utils/deep-util';
 import {select} from '../../utils/observable-util';
-import {RouterModel} from '../router/router-model';
-import {Finalizable} from '../registry';
+import {RouterModel} from '../../services/router/router-model';
+import {Finalizable} from '../../services/registry';
 import {define} from '../dependency';
 import {combineLatest, Subscription} from 'rxjs';
 import {fire, fireAlert, fireEvent} from '../../utils/event-util';
 import {CURRENT} from '../../utils/patch-set-util';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {ChangeModel} from '../change/change-model';
+import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {ChangeModel} from '../../services/change/change-model';
 import {Interaction, Timing} from '../../constants/reporting';
 import {assertIsDefined} from '../../utils/common-util';
 import {debounce, DelayedTask} from '../../utils/async-util';
 import {pluralize} from '../../utils/string-util';
-import {ReportingService} from '../gr-reporting/gr-reporting';
+import {ReportingService} from '../../services/gr-reporting/gr-reporting';
+import {Model} from '../model';
 
 export interface CommentState {
   /** undefined means 'still loading' */
@@ -222,12 +222,9 @@
 }
 
 export const commentsModelToken = define<CommentsModel>('comments-model');
-export class CommentsModel implements Finalizable {
-  private readonly privateState$: BehaviorSubject<CommentState> =
-    new BehaviorSubject(initialState);
-
+export class CommentsModel extends Model<CommentState> implements Finalizable {
   public readonly commentsLoading$ = select(
-    this.privateState$,
+    this.state$,
     commentState =>
       commentState.comments === undefined ||
       commentState.robotComments === undefined ||
@@ -235,29 +232,29 @@
   );
 
   public readonly comments$ = select(
-    this.privateState$,
+    this.state$,
     commentState => commentState.comments
   );
 
   public readonly drafts$ = select(
-    this.privateState$,
+    this.state$,
     commentState => commentState.drafts
   );
 
   public readonly portedComments$ = select(
-    this.privateState$,
+    this.state$,
     commentState => commentState.portedComments
   );
 
   public readonly discardedDrafts$ = select(
-    this.privateState$,
+    this.state$,
     commentState => commentState.discardedDrafts
   );
 
   // Emits a new value even if only a single draft is changed. Components should
   // aim to subsribe to something more specific.
   public readonly changeComments$ = select(
-    this.privateState$,
+    this.state$,
     commentState =>
       new ChangeComments(
         commentState.comments,
@@ -298,6 +295,7 @@
     readonly restApiService: RestApiService,
     readonly reporting: ReportingService
   ) {
+    super(initialState);
     this.subscriptions.push(
       this.discardedDrafts$.subscribe(x => (this.discardedDrafts = x))
     );
@@ -360,13 +358,13 @@
 
   // visible for testing
   updateState(reducer: (state: CommentState) => CommentState) {
-    const current = this.privateState$.getValue();
+    const current = this.subject$.getValue();
     this.setState(reducer({...current}));
   }
 
   // visible for testing
   setState(state: CommentState) {
-    this.privateState$.next(state);
+    this.subject$.next(state);
   }
 
   async reloadComments(changeNum: NumericChangeId): Promise<void> {
diff --git a/polygerrit-ui/app/services/comments/comments-model_test.ts b/polygerrit-ui/app/models/comments/comments-model_test.ts
similarity index 96%
rename from polygerrit-ui/app/services/comments/comments-model_test.ts
rename to polygerrit-ui/app/models/comments/comments-model_test.ts
index a8f2118..e713893 100644
--- a/polygerrit-ui/app/services/comments/comments-model_test.ts
+++ b/polygerrit-ui/app/models/comments/comments-model_test.ts
@@ -29,8 +29,8 @@
   TEST_NUMERIC_CHANGE_ID,
 } from '../../test/test-data-generators';
 import {stubRestApi, waitUntil, waitUntilCalled} from '../../test/test-utils';
-import {getAppContext} from '../app-context';
-import {GerritView} from '../router/router-model';
+import {getAppContext} from '../../services/app-context';
+import {GerritView} from '../../services/router/router-model';
 import {PathToCommentsInfoMap} from '../../types/common';
 
 suite('comments model tests', () => {
diff --git a/polygerrit-ui/app/services/config/config-model.ts b/polygerrit-ui/app/models/config/config-model.ts
similarity index 69%
rename from polygerrit-ui/app/services/config/config-model.ts
rename to polygerrit-ui/app/models/config/config-model.ts
index 91a77b8..e57a724 100644
--- a/polygerrit-ui/app/services/config/config-model.ts
+++ b/polygerrit-ui/app/models/config/config-model.ts
@@ -15,31 +15,24 @@
  * limitations under the License.
  */
 import {ConfigInfo, RepoName, ServerInfo} from '../../types/common';
-import {BehaviorSubject, from, Observable, of, Subscription} from 'rxjs';
+import {from, of, Subscription} from 'rxjs';
 import {switchMap} from 'rxjs/operators';
-import {Finalizable} from '../registry';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
-import {ChangeModel} from '../change/change-model';
+import {Finalizable} from '../../services/registry';
+import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
+import {ChangeModel} from '../../services/change/change-model';
 import {select} from '../../utils/observable-util';
+import {Model} from '../model';
+import {define} from '../dependency';
 
 export interface ConfigState {
   repoConfig?: ConfigInfo;
   serverConfig?: ServerInfo;
 }
 
-export class ConfigModel implements Finalizable {
-  // TODO: Figure out how to best enforce immutability of all states. Use Immer?
-  // Use DeepReadOnly?
-  private initialState: ConfigState = {};
-
-  private privateState$ = new BehaviorSubject(this.initialState);
-
-  // Re-exporting as Observable so that you can only subscribe, but not emit.
-  public configState$: Observable<ConfigState> =
-    this.privateState$.asObservable();
-
+export const configModelToken = define<ConfigModel>('config-model');
+export class ConfigModel extends Model<ConfigState> implements Finalizable {
   public repoConfig$ = select(
-    this.privateState$,
+    this.state$,
     configState => configState.repoConfig
   );
 
@@ -49,7 +42,7 @@
   );
 
   public serverConfig$ = select(
-    this.privateState$,
+    this.state$,
     configState => configState.serverConfig
   );
 
@@ -59,6 +52,7 @@
     readonly changeModel: ChangeModel,
     readonly restApiService: RestApiService
   ) {
+    super({});
     this.subscriptions = [
       from(this.restApiService.getConfig()).subscribe((config?: ServerInfo) => {
         this.updateServerConfig(config);
@@ -77,13 +71,13 @@
   }
 
   updateRepoConfig(repoConfig?: ConfigInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, repoConfig});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, repoConfig});
   }
 
   updateServerConfig(serverConfig?: ServerInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, serverConfig});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, serverConfig});
   }
 
   finalize() {
diff --git a/polygerrit-ui/app/services/dependency.ts b/polygerrit-ui/app/models/dependency.ts
similarity index 100%
rename from polygerrit-ui/app/services/dependency.ts
rename to polygerrit-ui/app/models/dependency.ts
diff --git a/polygerrit-ui/app/services/dependency_test.ts b/polygerrit-ui/app/models/dependency_test.ts
similarity index 100%
rename from polygerrit-ui/app/services/dependency_test.ts
rename to polygerrit-ui/app/models/dependency_test.ts
diff --git a/polygerrit-ui/app/models/model.ts b/polygerrit-ui/app/models/model.ts
new file mode 100644
index 0000000..4a7f5ac
--- /dev/null
+++ b/polygerrit-ui/app/models/model.ts
@@ -0,0 +1,42 @@
+/**
+ * @license
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed 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.
+ */
+
+import {BehaviorSubject, Observable} from 'rxjs';
+
+/**
+ * A Model stores a value <T> and controls changes to that value via `subject$`
+ * while allowing others to subscribe to value updates via the `state$`
+ * Observable.
+ *
+ * Typically a given Model subclass will provide:
+ *   1. an initial value
+ *   2. "reducers": functions for users to request changes to the value
+ *   3. "selectors": convenient sub-Observables that only contain updates for a
+ *          nested property from the value
+ *
+ *  Any new subscriber will immediately receive the current value.
+ */
+export abstract class Model<T> {
+  protected subject$: BehaviorSubject<T>;
+
+  public state$: Observable<T>;
+
+  constructor(initialState: T) {
+    this.subject$ = new BehaviorSubject(initialState);
+    this.state$ = this.subject$.asObservable();
+  }
+}
diff --git a/polygerrit-ui/app/services/user/user-model.ts b/polygerrit-ui/app/models/user/user-model.ts
similarity index 83%
rename from polygerrit-ui/app/services/user/user-model.ts
rename to polygerrit-ui/app/models/user/user-model.ts
index 441c09d..5e9ed3f 100644
--- a/polygerrit-ui/app/services/user/user-model.ts
+++ b/polygerrit-ui/app/models/user/user-model.ts
@@ -14,7 +14,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import {from, of, BehaviorSubject, Observable, Subscription} from 'rxjs';
+import {from, of, Observable, Subscription} from 'rxjs';
 import {switchMap} from 'rxjs/operators';
 import {
   DiffPreferencesInfo as DiffPreferencesInfoAPI,
@@ -29,10 +29,11 @@
   createDefaultPreferences,
   createDefaultDiffPrefs,
 } from '../../constants/constants';
-import {RestApiService} from '../gr-rest-api/gr-rest-api';
+import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
 import {DiffPreferencesInfo} from '../../types/diff';
-import {Finalizable} from '../registry';
+import {Finalizable} from '../../services/registry';
 import {select} from '../../utils/observable-util';
+import {Model} from '../model';
 
 export interface UserState {
   /**
@@ -44,15 +45,9 @@
   capabilities?: AccountCapabilityInfo;
 }
 
-export class UserModel implements Finalizable {
-  private readonly privateState$: BehaviorSubject<UserState> =
-    new BehaviorSubject({
-      preferences: createDefaultPreferences(),
-      diffPreferences: createDefaultDiffPrefs(),
-    });
-
+export class UserModel extends Model<UserState> implements Finalizable {
   readonly account$: Observable<AccountDetailInfo | undefined> = select(
-    this.privateState$,
+    this.state$,
     userState => userState.account
   );
 
@@ -63,7 +58,7 @@
   );
 
   readonly capabilities$: Observable<AccountCapabilityInfo | undefined> =
-    select(this.userState$, userState => userState.capabilities);
+    select(this.state$, userState => userState.capabilities);
 
   readonly isAdmin$: Observable<boolean> = select(
     this.capabilities$,
@@ -71,12 +66,12 @@
   );
 
   readonly preferences$: Observable<PreferencesInfo> = select(
-    this.privateState$,
+    this.state$,
     userState => userState.preferences
   );
 
   readonly diffPreferences$: Observable<DiffPreferencesInfo> = select(
-    this.privateState$,
+    this.state$,
     userState => userState.diffPreferences
   );
 
@@ -87,11 +82,11 @@
 
   private subscriptions: Subscription[] = [];
 
-  get userState$(): Observable<UserState> {
-    return this.privateState$;
-  }
-
   constructor(readonly restApiService: RestApiService) {
+    super({
+      preferences: createDefaultPreferences(),
+      diffPreferences: createDefaultDiffPrefs(),
+    });
     this.subscriptions = [
       from(this.restApiService.getAccount()).subscribe(
         (account?: AccountDetailInfo) => {
@@ -167,22 +162,22 @@
   }
 
   setPreferences(preferences: PreferencesInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, preferences});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, preferences});
   }
 
   setDiffPreferences(diffPreferences: DiffPreferencesInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, diffPreferences});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, diffPreferences});
   }
 
   setCapabilities(capabilities?: AccountCapabilityInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, capabilities});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, capabilities});
   }
 
   private setAccount(account?: AccountDetailInfo) {
-    const current = this.privateState$.getValue();
-    this.privateState$.next({...current, account});
+    const current = this.subject$.getValue();
+    this.subject$.next({...current, account});
   }
 }
diff --git a/polygerrit-ui/app/services/app-context-init.ts b/polygerrit-ui/app/services/app-context-init.ts
index 0cb3876..3f34061 100644
--- a/polygerrit-ui/app/services/app-context-init.ts
+++ b/polygerrit-ui/app/services/app-context-init.ts
@@ -16,7 +16,7 @@
  */
 import {AppContext} from './app-context';
 import {create, Finalizable, Registry} from './registry';
-import {DependencyToken} from './dependency';
+import {DependencyToken} from '../models/dependency';
 import {FlagsServiceImplementation} from './flags/flags_impl';
 import {GrReporting} from './gr-reporting/gr-reporting_impl';
 import {EventEmitter} from './gr-event-interface/gr-event-interface_impl';
@@ -26,13 +26,16 @@
 import {ChecksModel} from './checks/checks-model';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
 import {GrStorageService} from './storage/gr-storage_impl';
-import {UserModel} from './user/user-model';
-import {CommentsModel, commentsModelToken} from './comments/comments-model';
+import {UserModel} from '../models/user/user-model';
+import {
+  CommentsModel,
+  commentsModelToken,
+} from '../models/comments/comments-model';
 import {RouterModel} from './router/router-model';
 import {ShortcutsService} from './shortcuts/shortcuts-service';
 import {assertIsDefined} from '../utils/common-util';
-import {ConfigModel} from './config/config-model';
-import {BrowserModel, browserModelToken} from './browser/browser-model';
+import {ConfigModel, configModelToken} from '../models/config/config-model';
+import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
 
 /**
  * The AppContext lazy initializator for all services
@@ -78,13 +81,6 @@
       return new GrJsApiInterface(reportingService!);
     },
     storageService: (_ctx: Partial<AppContext>) => new GrStorageService(),
-    configModel: (ctx: Partial<AppContext>) => {
-      const changeModel = ctx.changeModel;
-      const restApiService = ctx.restApiService;
-      assertIsDefined(changeModel, 'changeModel');
-      assertIsDefined(restApiService, 'restApiService');
-      return new ConfigModel(changeModel, restApiService);
-    },
     userModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new UserModel(ctx.restApiService!);
@@ -113,5 +109,11 @@
   );
   dependencies.set(commentsModelToken, commentsModel);
 
+  const configModel = new ConfigModel(
+    appContext.changeModel,
+    appContext.restApiService
+  );
+  dependencies.set(configModelToken, configModel);
+
   return dependencies;
 }
diff --git a/polygerrit-ui/app/services/app-context.ts b/polygerrit-ui/app/services/app-context.ts
index 2e02ebf..6d6afc9 100644
--- a/polygerrit-ui/app/services/app-context.ts
+++ b/polygerrit-ui/app/services/app-context.ts
@@ -24,10 +24,9 @@
 import {ChecksModel} from './checks/checks-model';
 import {JsApiService} from '../elements/shared/gr-js-api-interface/gr-js-api-types';
 import {StorageService} from './storage/gr-storage';
-import {UserModel} from './user/user-model';
+import {UserModel} from '../models/user/user-model';
 import {RouterModel} from './router/router-model';
 import {ShortcutsService} from './shortcuts/shortcuts-service';
-import {ConfigModel} from './config/config-model';
 
 export interface AppContext {
   routerModel: RouterModel;
@@ -40,7 +39,6 @@
   checksModel: ChecksModel;
   jsApiService: JsApiService;
   storageService: StorageService;
-  configModel: ConfigModel;
   userModel: UserModel;
   shortcutsService: ShortcutsService;
 }
diff --git a/polygerrit-ui/app/services/change/change-model.ts b/polygerrit-ui/app/services/change/change-model.ts
index e410dd5..0ba1e6c 100644
--- a/polygerrit-ui/app/services/change/change-model.ts
+++ b/polygerrit-ui/app/services/change/change-model.ts
@@ -25,7 +25,6 @@
   combineLatest,
   from,
   fromEvent,
-  BehaviorSubject,
   Observable,
   Subscription,
   forkJoin,
@@ -50,6 +49,7 @@
 import {Finalizable} from '../registry';
 import {select} from '../../utils/observable-util';
 import {assertIsDefined} from '../../utils/common-util';
+import {Model} from '../../models/model';
 
 export enum LoadingStatus {
   NOT_LOADED = 'NOT_LOADED',
@@ -111,24 +111,19 @@
   loadingStatus: LoadingStatus.NOT_LOADED,
 };
 
-export class ChangeModel implements Finalizable {
-  private readonly privateState$ = new BehaviorSubject(initialState);
-
-  public readonly changeState$: Observable<ChangeState> =
-    this.privateState$.asObservable();
-
+export class ChangeModel extends Model<ChangeState> implements Finalizable {
   public readonly change$ = select(
-    this.privateState$,
+    this.state$,
     changeState => changeState.change
   );
 
   public readonly changeLoadingStatus$ = select(
-    this.privateState$,
+    this.state$,
     changeState => changeState.loadingStatus
   );
 
   public readonly diffPath$ = select(
-    this.privateState$,
+    this.state$,
     changeState => changeState?.diffPath
   );
 
@@ -156,7 +151,7 @@
      * out inconsistent state, e.g. router changeNum already updated, change not
      * yet reset to undefined.
      */
-    combineLatest([this.routerModel.routerState$, this.changeState$])
+    combineLatest([this.routerModel.state$, this.state$])
       .pipe(
         filter(([routerState, changeState]) => {
           const changeNum = changeState.change?._number;
@@ -184,6 +179,7 @@
     readonly routerModel: RouterModel,
     readonly restApiService: RestApiService
   ) {
+    super(initialState);
     this.subscriptions = [
       combineLatest([this.routerModel.routerChangeNum$, this.reload$])
         .pipe(
@@ -221,7 +217,7 @@
 
   // Temporary workaround until path is derived in the model itself.
   updatePath(diffPath?: string) {
-    const current = this.getState();
+    const current = this.subject$.getValue();
     this.setState({...current, diffPath});
   }
 
@@ -231,7 +227,7 @@
    * demand. So here it is for your convenience.
    */
   getChange() {
-    return this.getState().change;
+    return this.subject$.getValue().change;
   }
 
   /**
@@ -275,7 +271,7 @@
    * a new change number, but an old change.
    */
   private updateStateLoading(changeNum: NumericChangeId) {
-    const current = this.getState();
+    const current = this.subject$.getValue();
     const reloading = current.change?._number === changeNum;
     this.setState({
       ...current,
@@ -288,7 +284,7 @@
 
   // Private but used in tests.
   updateStateChange(change?: ParsedChangeInfo) {
-    const current = this.getState();
+    const current = this.subject$.getValue();
     this.setState({
       ...current,
       change,
@@ -297,12 +293,8 @@
     });
   }
 
-  getState(): ChangeState {
-    return this.privateState$.getValue();
-  }
-
   // Private but used in tests
   setState(state: ChangeState) {
-    this.privateState$.next(state);
+    this.subject$.next(state);
   }
 }
diff --git a/polygerrit-ui/app/services/change/change-model_test.ts b/polygerrit-ui/app/services/change/change-model_test.ts
index edbc403..e28fb69 100644
--- a/polygerrit-ui/app/services/change/change-model_test.ts
+++ b/polygerrit-ui/app/services/change/change-model_test.ts
@@ -110,7 +110,7 @@
     let state: ChangeState | undefined = {
       loadingStatus: LoadingStatus.NOT_LOADED,
     };
-    changeModel.changeState$
+    changeModel.state$
       .pipe(takeUntil(testCompleted))
       .subscribe(s => (state = s));
 
@@ -139,7 +139,7 @@
     let state: ChangeState | undefined = {
       loadingStatus: LoadingStatus.NOT_LOADED,
     };
-    changeModel.changeState$
+    changeModel.state$
       .pipe(takeUntil(testCompleted))
       .subscribe(s => (state = s));
     changeModel.routerModel.setState({
@@ -168,7 +168,7 @@
     let state: ChangeState | undefined = {
       loadingStatus: LoadingStatus.NOT_LOADED,
     };
-    changeModel.changeState$
+    changeModel.state$
       .pipe(takeUntil(testCompleted))
       .subscribe(s => (state = s));
     changeModel.routerModel.setState({
@@ -206,7 +206,7 @@
     let state: ChangeState | undefined = {
       loadingStatus: LoadingStatus.NOT_LOADED,
     };
-    changeModel.changeState$
+    changeModel.state$
       .pipe(takeUntil(testCompleted))
       .subscribe(s => (state = s));
     changeModel.routerModel.setState({
diff --git a/polygerrit-ui/app/services/checks/checks-model.ts b/polygerrit-ui/app/services/checks/checks-model.ts
index a1d8018..2bdee51 100644
--- a/polygerrit-ui/app/services/checks/checks-model.ts
+++ b/polygerrit-ui/app/services/checks/checks-model.ts
@@ -55,6 +55,7 @@
 import {Execution} from '../../constants/reporting';
 import {fireAlert, fireEvent} from '../../utils/event-util';
 import {RouterModel} from '../router/router-model';
+import {Model} from '../../models/model';
 
 /**
  * The checks model maintains the state of checks for two patchsets: the latest
@@ -150,7 +151,7 @@
   [name: string]: string;
 }
 
-export class ChecksModel implements Finalizable {
+export class ChecksModel extends Model<ChecksState> implements Finalizable {
   private readonly providers: {[name: string]: ChecksProvider} = {};
 
   private readonly reloadSubjects: {[name: string]: Subject<void>} = {};
@@ -169,25 +170,14 @@
 
   private subscriptions: Subscription[] = [];
 
-  private readonly privateState$ = new BehaviorSubject<ChecksState>({
-    pluginStateLatest: {},
-    pluginStateSelected: {},
-  });
-
-  public checksState$: Observable<ChecksState> =
-    this.privateState$.asObservable();
-
   public checksSelectedPatchsetNumber$ = select(
-    this.checksState$,
+    this.state$,
     state => state.patchsetNumberSelected
   );
 
-  public checksLatest$ = select(
-    this.checksState$,
-    state => state.pluginStateLatest
-  );
+  public checksLatest$ = select(this.state$, state => state.pluginStateLatest);
 
-  public checksSelected$ = select(this.checksState$, state =>
+  public checksSelected$ = select(this.state$, state =>
     state.patchsetNumberSelected
       ? state.pluginStateSelected
       : state.pluginStateLatest
@@ -325,6 +315,10 @@
     readonly changeModel: ChangeModel,
     readonly reporting: ReportingService
   ) {
+    super({
+      pluginStateLatest: {},
+      pluginStateSelected: {},
+    });
     this.subscriptions = [
       this.changeModel.changeNum$.subscribe(x => (this.changeNum = x)),
       this.checkToPluginMap$.subscribe(map => {
@@ -365,13 +359,13 @@
       s.unsubscribe();
     }
     this.subscriptions = [];
-    this.privateState$.complete();
+    this.subject$.complete();
   }
 
   // Must only be used by the checks service or whatever is in control of this
   // model.
   updateStateSetProvider(pluginName: string, patchset: ChecksPatchset) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     pluginState[pluginName] = {
       pluginName,
@@ -381,7 +375,7 @@
       actions: [],
       links: [],
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   getPluginState(
@@ -398,13 +392,13 @@
   }
 
   updateStateSetLoading(pluginName: string, patchset: ChecksPatchset) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     pluginState[pluginName] = {
       ...pluginState[pluginName],
       loading: true,
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   updateStateSetError(
@@ -412,7 +406,7 @@
     errorMessage: string,
     patchset: ChecksPatchset
   ) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     pluginState[pluginName] = {
       ...pluginState[pluginName],
@@ -423,7 +417,7 @@
       runs: [],
       actions: [],
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   updateStateSetNotLoggedIn(
@@ -431,7 +425,7 @@
     loginCallback: () => void,
     patchset: ChecksPatchset
   ) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     pluginState[pluginName] = {
       ...pluginState[pluginName],
@@ -442,7 +436,7 @@
       runs: [],
       actions: [],
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   updateStateSetResults(
@@ -460,7 +454,7 @@
         (a, b) => (a.attempt ?? -1) - (b.attempt ?? -1)
       );
     }
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     pluginState[pluginName] = {
       ...pluginState[pluginName],
@@ -490,7 +484,7 @@
       actions: [...actions],
       links: [...links],
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   updateStateUpdateResult(
@@ -499,7 +493,7 @@
     updatedResult: CheckResultApi,
     patchset: ChecksPatchset
   ) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     const pluginState = this.getPluginState(nextState, patchset);
     let runUpdated = false;
     const runs: CheckRun[] = pluginState[pluginName].runs.map(run => {
@@ -529,13 +523,13 @@
       ...pluginState[pluginName],
       runs,
     };
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   updateStateSetPatchset(patchsetNumber?: PatchSetNumber) {
-    const nextState = {...this.privateState$.getValue()};
+    const nextState = {...this.subject$.getValue()};
     nextState.patchsetNumberSelected = patchsetNumber;
-    this.privateState$.next(nextState);
+    this.subject$.next(nextState);
   }
 
   setPatchset(num?: PatchSetNumber) {
diff --git a/polygerrit-ui/app/services/router/router-model.ts b/polygerrit-ui/app/services/router/router-model.ts
index 73dee78..5bd228b 100644
--- a/polygerrit-ui/app/services/router/router-model.ts
+++ b/polygerrit-ui/app/services/router/router-model.ts
@@ -15,10 +15,11 @@
  * limitations under the License.
  */
 
-import {BehaviorSubject, Observable} from 'rxjs';
+import {Observable} from 'rxjs';
 import {distinctUntilChanged, map} from 'rxjs/operators';
 import {Finalizable} from '../registry';
 import {NumericChangeId, PatchSetNum} from '../../types/common';
+import {Model} from '../../models/model';
 
 export enum GerritView {
   ADMIN = 'admin',
@@ -43,9 +44,7 @@
   patchNum?: PatchSetNum;
 }
 
-export class RouterModel implements Finalizable {
-  private readonly privateState$ = new BehaviorSubject<RouterState>({});
-
+export class RouterModel extends Model<RouterState> implements Finalizable {
   readonly routerView$: Observable<GerritView | undefined>;
 
   readonly routerChangeNum$: Observable<NumericChangeId | undefined>;
@@ -53,15 +52,16 @@
   readonly routerPatchNum$: Observable<PatchSetNum | undefined>;
 
   constructor() {
-    this.routerView$ = this.privateState$.pipe(
+    super({});
+    this.routerView$ = this.state$.pipe(
       map(state => state.view),
       distinctUntilChanged()
     );
-    this.routerChangeNum$ = this.privateState$.pipe(
+    this.routerChangeNum$ = this.state$.pipe(
       map(state => state.changeNum),
       distinctUntilChanged()
     );
-    this.routerPatchNum$ = this.privateState$.pipe(
+    this.routerPatchNum$ = this.state$.pipe(
       map(state => state.patchNum),
       distinctUntilChanged()
     );
@@ -69,18 +69,15 @@
 
   finalize() {}
 
+  // Private but used in tests
   setState(state: RouterState) {
-    this.privateState$.next(state);
+    this.subject$.next(state);
   }
 
   updateState(partial: Partial<RouterState>) {
-    this.privateState$.next({
-      ...this.privateState$.getValue(),
+    this.subject$.next({
+      ...this.subject$.getValue(),
       ...partial,
     });
   }
-
-  get routerState$(): Observable<RouterState> {
-    return this.privateState$;
-  }
 }
diff --git a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
index 79f2993..41591a2 100644
--- a/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
+++ b/polygerrit-ui/app/services/shortcuts/shortcuts-service.ts
@@ -33,7 +33,7 @@
 } from '../../utils/dom-util';
 import {ReportingService} from '../gr-reporting/gr-reporting';
 import {Finalizable} from '../registry';
-import {UserModel} from '../user/user-model';
+import {UserModel} from '../../models/user/user-model';
 
 export type SectionView = Array<{binding: string[][]; text: string}>;
 
diff --git a/polygerrit-ui/app/test/common-test-setup.ts b/polygerrit-ui/app/test/common-test-setup.ts
index 19cb790..23c4141 100644
--- a/polygerrit-ui/app/test/common-test-setup.ts
+++ b/polygerrit-ui/app/test/common-test-setup.ts
@@ -53,7 +53,7 @@
   DependencyError,
   DependencyToken,
   Provider,
-} from '../services/dependency';
+} from '../models/dependency';
 
 declare global {
   interface Window {
diff --git a/polygerrit-ui/app/test/test-app-context-init.ts b/polygerrit-ui/app/test/test-app-context-init.ts
index 491a891..cd6e5c5 100644
--- a/polygerrit-ui/app/test/test-app-context-init.ts
+++ b/polygerrit-ui/app/test/test-app-context-init.ts
@@ -17,7 +17,7 @@
 
 // Init app context before any other imports
 import {create, Registry, Finalizable} from '../services/registry';
-import {DependencyToken} from '../services/dependency';
+import {DependencyToken} from '../models/dependency';
 import {assertIsDefined} from '../utils/common-util';
 import {AppContext} from '../services/app-context';
 import {grReportingMock} from '../services/gr-reporting/gr-reporting_mock';
@@ -29,18 +29,15 @@
 import {ChangeModel} from '../services/change/change-model';
 import {ChecksModel} from '../services/checks/checks-model';
 import {GrJsApiInterface} from '../elements/shared/gr-js-api-interface/gr-js-api-interface-element';
-import {UserModel} from '../services/user/user-model';
+import {UserModel} from '../models/user/user-model';
 import {
   CommentsModel,
   commentsModelToken,
-} from '../services/comments/comments-model';
+} from '../models/comments/comments-model';
 import {RouterModel} from '../services/router/router-model';
 import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
-import {ConfigModel} from '../services/config/config-model';
-import {
-  BrowserModel,
-  browserModelToken,
-} from '../services/browser/browser-model';
+import {ConfigModel, configModelToken} from '../models/config/config-model';
+import {BrowserModel, browserModelToken} from '../models/browser/browser-model';
 
 export function createTestAppContext(): AppContext & Finalizable {
   const appRegistry: Registry<AppContext> = {
@@ -75,11 +72,6 @@
       return new GrJsApiInterface(ctx.reportingService!);
     },
     storageService: (_ctx: Partial<AppContext>) => grStorageMock,
-    configModel: (ctx: Partial<AppContext>) => {
-      assertIsDefined(ctx.changeModel, 'changeModel');
-      assertIsDefined(ctx.restApiService, 'restApiService');
-      return new ConfigModel(ctx.changeModel!, ctx.restApiService!);
-    },
     userModel: (ctx: Partial<AppContext>) => {
       assertIsDefined(ctx.restApiService, 'restApiService');
       return new UserModel(ctx.restApiService!);
@@ -115,5 +107,9 @@
     );
   dependencies.set(commentsModelToken, commentsModel);
 
+  const configModel = () =>
+    new ConfigModel(appContext.changeModel, appContext.restApiService);
+  dependencies.set(configModelToken, configModel);
+
   return dependencies;
 }
diff --git a/polygerrit-ui/app/test/test-utils.ts b/polygerrit-ui/app/test/test-utils.ts
index 7652039..fbc2433 100644
--- a/polygerrit-ui/app/test/test-utils.ts
+++ b/polygerrit-ui/app/test/test-utils.ts
@@ -23,7 +23,7 @@
 import {StorageService} from '../services/storage/gr-storage';
 import {AuthService} from '../services/gr-auth/gr-auth';
 import {ReportingService} from '../services/gr-reporting/gr-reporting';
-import {UserModel} from '../services/user/user-model';
+import {UserModel} from '../models/user/user-model';
 import {ShortcutsService} from '../services/shortcuts/shortcuts-service';
 import {queryAndAssert, query} from '../utils/common-util';
 import {FlagsService} from '../services/flags/flags';
diff --git a/polygerrit-ui/app/tsconfig.json b/polygerrit-ui/app/tsconfig.json
index 5040496..a335926 100644
--- a/polygerrit-ui/app/tsconfig.json
+++ b/polygerrit-ui/app/tsconfig.json
@@ -87,6 +87,7 @@
     "embed/**/*",
     "gr-diff/**/*",
     "mixins/**/*",
+    "models/**/*",
     "samples/**/*",
     "scripts/**/*",
     "services/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel.json b/polygerrit-ui/app/tsconfig_bazel.json
index dfd2078..cd83fc0 100644
--- a/polygerrit-ui/app/tsconfig_bazel.json
+++ b/polygerrit-ui/app/tsconfig_bazel.json
@@ -16,6 +16,7 @@
     "embed/**/*",
     "gr-diff/**/*",
     "mixins/**/*",
+    "models/**/*",
     "samples/**/*",
     "scripts/**/*",
     "services/**/*",
diff --git a/polygerrit-ui/app/tsconfig_bazel_test.json b/polygerrit-ui/app/tsconfig_bazel_test.json
index 7137e23..be3c934 100644
--- a/polygerrit-ui/app/tsconfig_bazel_test.json
+++ b/polygerrit-ui/app/tsconfig_bazel_test.json
@@ -20,6 +20,7 @@
     "embed/**/*",
     "gr-diff/**/*",
     "mixins/**/*",
+    "models/**/*",
     "samples/**/*",
     "scripts/**/*",
     "services/**/*",
diff --git a/tools/BUILD b/tools/BUILD
index 48cc854..7fadb64 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -276,7 +276,7 @@
         "-Xep:JavaPeriodGetDays:ERROR",
         "-Xep:JavaTimeDefaultTimeZone:ERROR",
         "-Xep:JavaUtilDate:WARN",
-        # "-Xep:JdkObsolete:WARN",
+        "-Xep:JdkObsolete:ERROR",
         "-Xep:JodaConstructors:ERROR",
         "-Xep:JodaDateTimeConstants:ERROR",
         "-Xep:JodaDurationWithMillis:ERROR",