diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 8b79b5e9f0c17d972a611d2aac4959f79a375b90..0a498fde95a46e1a25fd36e9603f19784499bcdf 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -3,8 +3,6 @@
 class BugzillaService < IssueTrackerService
   validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 
-  prop_accessor :project_url, :issues_url, :new_issue_url
-
   def default_title
     'Bugzilla'
   end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 535fcf6b94e7dea38b55d58abad908cb7b86ca2a..dbc42b1b86d6f919e4baf431ac014a66a4a006cb 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -3,8 +3,6 @@
 class CustomIssueTrackerService < IssueTrackerService
   validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 
-  prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
-
   def default_title
     'Custom Issue Tracker'
   end
diff --git a/app/models/project_services/data_fields.rb b/app/models/project_services/data_fields.rb
index 438d85098c8cc2e3b5e0f36a418fa4b92acc4512..0f5385f8ce2393f36d3e8653116748390dd68712 100644
--- a/app/models/project_services/data_fields.rb
+++ b/app/models/project_services/data_fields.rb
@@ -3,8 +3,56 @@
 module DataFields
   extend ActiveSupport::Concern
 
+  class_methods do
+    # Provide convenient accessor methods for data fields.
+    # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+    def data_field(*args)
+      args.each do |arg|
+        self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+          unless method_defined?(arg)
+            def #{arg}
+              data_fields.send('#{arg}') || (properties && properties['#{arg}'])
+            end
+          end
+
+          def #{arg}=(value)
+            @old_data_fields ||= {}
+            @old_data_fields['#{arg}'] ||= #{arg} # set only on the first assignment, IOW we remember the original value only
+            data_fields.send('#{arg}=', value)
+          end
+
+          def #{arg}_touched?
+            @old_data_fields ||= {}
+            @old_data_fields.has_key?('#{arg}')
+          end
+
+          def #{arg}_changed?
+            #{arg}_touched? && @old_data_fields['#{arg}'] != #{arg}
+          end
+
+          def #{arg}_was
+            return unless #{arg}_touched?
+            return if data_fields.persisted? # arg_was does not work for attr_encrypted
+
+            legacy_properties_data['#{arg}']
+          end
+        RUBY
+      end
+    end
+  end
+
   included do
-    has_one :issue_tracker_data
-    has_one :jira_tracker_data
+    has_one :issue_tracker_data, autosave: true
+    has_one :jira_tracker_data, autosave: true
+
+    def data_fields
+      raise NotImplementedError
+    end
+
+    def data_fields_present?
+      data_fields.persisted?
+    rescue NotImplementedError
+      false
+    end
   end
 end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 51032932eabed5cda89057bda846b9eea240ee56..ec28602b5e6ffaf55ec2ac13af0ecfcec97b70c7 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -5,8 +5,6 @@ class GitlabIssueTrackerService < IssueTrackerService
 
   validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 
-  prop_accessor :project_url, :issues_url, :new_issue_url
-
   default_value_for :default, true
 
   def default_title
diff --git a/app/models/project_services/issue_tracker_data.rb b/app/models/project_services/issue_tracker_data.rb
index 2c1d28ed421aad20f78bb3da7697b3dac259c858..b1d67657fe6fa0536bbe3cbb933eb3e192e4b415 100644
--- a/app/models/project_services/issue_tracker_data.rb
+++ b/app/models/project_services/issue_tracker_data.rb
@@ -6,9 +6,6 @@ class IssueTrackerData < ApplicationRecord
   delegate :activated?, to: :service, allow_nil: true
 
   validates :service, presence: true
-  validates :project_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
-  validates :issues_url, presence: true, public_url: { enforce_sanitization: true }, if: :activated?
-  validates :new_issue_url, public_url: { enforce_sanitization: true }, if: :activated?
 
   def self.encryption_options
     {
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index b6ad46513dbf8295a81835f61a5ae3c7bad36156..c201bd2ea185127adaee619839a459c5f8e8cb97 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -3,9 +3,14 @@
 class IssueTrackerService < Service
   validate :one_issue_tracker, if: :activated?, on: :manual_change
 
+  # TODO: we can probably just delegate as part of
+  # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+  data_field :project_url, :issues_url, :new_issue_url
+
   default_value_for :category, 'issue_tracker'
 
-  before_save :handle_properties
+  before_validation :handle_properties
+  before_validation :set_default_data, on: :create
 
   # Pattern used to extract links from comments
   # Override this method on services that uses different patterns
@@ -43,12 +48,31 @@ def description
   end
 
   def handle_properties
-    properties.slice('title', 'description').each do |key, _|
+    # this has been moved from initialize_properties and should be improved
+    # as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+    return unless properties
+
+    @legacy_properties_data = properties.dup
+    data_values = properties.slice!('title', 'description')
+    properties.each do |key, _|
       current_value = self.properties.delete(key)
       value = attribute_changed?(key) ? attribute_change(key).last : current_value
 
       write_attribute(key, value)
     end
+
+    data_values.reject! { |key| data_fields.changed.include?(key) }
+    data_fields.assign_attributes(data_values) if data_values.present?
+
+    self.properties = {}
+  end
+
+  def legacy_properties_data
+    @legacy_properties_data ||= {}
+  end
+
+  def data_fields
+    issue_tracker_data || self.build_issue_tracker_data
   end
 
   def default?
@@ -56,7 +80,7 @@ def default?
   end
 
   def issue_url(iid)
-    self.issues_url.gsub(':id', iid.to_s)
+    issues_url.gsub(':id', iid.to_s)
   end
 
   def issue_tracker_path
@@ -80,25 +104,22 @@ def fields
     ]
   end
 
+  def initialize_properties
+    {}
+  end
+
   # Initialize with default properties values
-  # or receive a block with custom properties
-  def initialize_properties(&block)
-    return unless properties.nil?
-
-    if enabled_in_gitlab_config
-      if block_given?
-        yield
-      else
-        self.properties = {
-          title: issues_tracker['title'],
-          project_url: issues_tracker['project_url'],
-          issues_url: issues_tracker['issues_url'],
-          new_issue_url: issues_tracker['new_issue_url']
-        }
-      end
-    else
-      self.properties = {}
-    end
+  def set_default_data
+    return unless issues_tracker.present?
+
+    self.title ||= issues_tracker['title']
+
+    # we don't want to override if we have set something
+    return if project_url || issues_url || new_issue_url
+
+    data_fields.project_url = issues_tracker['project_url']
+    data_fields.issues_url = issues_tracker['issues_url']
+    data_fields.new_issue_url = issues_tracker['new_issue_url']
   end
 
   def self.supported_events
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 0728c83005eee1a8f9d1edd5228c8a4aa3c35151..61ae78a0b95706c6c318b47cb3909fb27567b79f 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -17,7 +17,10 @@ class JiraService < IssueTrackerService
   # Jira Cloud version is deprecating authentication via username and password.
   # We should use username/password for Jira Server and email/api_token for Jira Cloud,
   # for more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/49936.
-  prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id
+
+  # TODO: we can probably just delegate as part of
+  # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+  data_field :username, :password, :url, :api_url, :jira_issue_transition_id
 
   before_update :reset_password
 
@@ -35,24 +38,34 @@ def self.reference_pattern(only_long: true)
   end
 
   def initialize_properties
-    super do
-      self.properties = {
-        url: issues_tracker['url'],
-        api_url: issues_tracker['api_url']
-      }
-    end
+    {}
+  end
+
+  def data_fields
+    jira_tracker_data || self.build_jira_tracker_data
   end
 
   def reset_password
-    self.password = nil if reset_password?
+    data_fields.password = nil if reset_password?
+  end
+
+  def set_default_data
+    return unless issues_tracker.present?
+
+    self.title ||= issues_tracker['title']
+
+    return if url
+
+    data_fields.url ||= issues_tracker['url']
+    data_fields.api_url ||= issues_tracker['api_url']
   end
 
   def options
     url = URI.parse(client_url)
 
     {
-      username: self.username,
-      password: self.password,
+      username: username,
+      password: password,
       site: URI.join(url, '/').to_s, # Intended to find the root
       context_path: url.path,
       auth_type: :basic,
diff --git a/app/models/project_services/jira_tracker_data.rb b/app/models/project_services/jira_tracker_data.rb
index 4f528e3d81b36b666b956986a706357f51dcc2f2..e4e0f64150a23eb2749860cd96f21e21cf63d59f 100644
--- a/app/models/project_services/jira_tracker_data.rb
+++ b/app/models/project_services/jira_tracker_data.rb
@@ -6,13 +6,6 @@ class JiraTrackerData < ApplicationRecord
   delegate :activated?, to: :service, allow_nil: true
 
   validates :service, presence: true
-  validates :url, public_url: { enforce_sanitization: true }, presence: true, if: :activated?
-  validates :api_url, public_url: { enforce_sanitization: true }, allow_blank: true
-  validates :username, presence: true, if: :activated?
-  validates :password, presence: true, if: :activated?
-  validates :jira_issue_transition_id,
-            format: { with: Gitlab::Regex.jira_transition_id_regex, message: s_("JiraService|transition ids can have only numbers which can be split with , or ;") },
-            allow_blank: true
 
   def self.encryption_options
     {
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index 5ca057ca833ab206eca7ca6cd0dce937e58fcda9..a4ca0d20669f89952fa44a7ae01bd4c71a844e2d 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -3,8 +3,6 @@
 class RedmineService < IssueTrackerService
   validates :project_url, :issues_url, :new_issue_url, presence: true, public_url: true, if: :activated?
 
-  prop_accessor :project_url, :issues_url, :new_issue_url
-
   def default_title
     'Redmine'
   end
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index f9de1f7dc496623458198b94bec28e560c269237..0416eaa5be0bb1d3f33f5e2e89a5c5408861f367 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -3,8 +3,6 @@
 class YoutrackService < IssueTrackerService
   validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
 
-  prop_accessor :project_url, :issues_url
-
   # {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
   def self.reference_pattern(only_long: false)
     if only_long
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 6ca9cc40026136d3bbac2f12343e0af1b863d002..4018ce900e192b8bae9e5b76d7509f3e73332e3b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1044,7 +1044,12 @@ class ProjectService < Grape::Entity
       expose :job_events
       # Expose serialized properties
       expose :properties do |service, options|
-        service.properties.slice(*service.api_field_names)
+        # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+        if service.data_fields_present?
+          service.data_fields.as_json.slice(*service.api_field_names)
+        else
+          service.properties.slice(*service.api_field_names)
+        end
       end
     end
 
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 5cd54c302fcc4eda87b8ad98c112e1f74ad90af2..c6c2876033d2fad7cbfd8e48cca90ec94d6988d2 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -3,6 +3,7 @@
 module Gitlab
   class UsageData
     APPROXIMATE_COUNT_MODELS = [Label, MergeRequest, Note, Todo].freeze
+    BATCH_SIZE = 100
 
     class << self
       def data(force_refresh: false)
@@ -13,10 +14,10 @@ def data(force_refresh: false)
 
       def uncached_data
         license_usage_data.merge(system_usage_data)
-                          .merge(features_usage_data)
-                          .merge(components_usage_data)
-                          .merge(cycle_analytics_usage_data)
-                          .merge(usage_counters)
+          .merge(features_usage_data)
+          .merge(components_usage_data)
+          .merge(cycle_analytics_usage_data)
+          .merge(usage_counters)
       end
 
       def to_json(force_refresh: false)
@@ -96,9 +97,8 @@ def system_usage_data
             todos: count(Todo),
             uploads: count(Upload),
             web_hooks: count(WebHook)
-          }
-          .merge(services_usage)
-          .merge(approximate_counts)
+          }.merge(services_usage)
+            .merge(approximate_counts)
         }.tap do |data|
           data[:counts][:user_preferences] = user_preferences_usage
         end
@@ -173,17 +173,34 @@ def services_usage
       def jira_usage
         # Jira Cloud does not support custom domains as per https://jira.atlassian.com/browse/CLOUD-6999
         # so we can just check for subdomains of atlassian.net
-        services = count(
-          Service.unscoped.where(type: :JiraService, active: true)
-            .group("CASE WHEN properties LIKE '%.atlassian.net%' THEN 'cloud' ELSE 'server' END"),
-          fallback: Hash.new(-1)
-        )
 
-        {
-          projects_jira_server_active: services['server'] || 0,
-          projects_jira_cloud_active: services['cloud'] || 0,
-          projects_jira_active: services['server'] == -1 ? -1 : services.values.sum
+        results = {
+          projects_jira_server_active: 0,
+          projects_jira_cloud_active: 0,
+          projects_jira_active: -1
         }
+
+        Service.unscoped
+          .where(type: :JiraService, active: true)
+          .includes(:jira_tracker_data)
+          .find_in_batches(batch_size: BATCH_SIZE) do |services|
+
+          counts = services.group_by do |service|
+            # TODO: Simplify as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
+            service_url = service.data_fields&.url || (service.properties && service.properties['url'])
+            service_url&.include?('.atlassian.net') ? :cloud : :server
+          end
+
+          results[:projects_jira_server_active] += counts[:server].count if counts[:server]
+          results[:projects_jira_cloud_active] += counts[:cloud].count if counts[:cloud]
+          if results[:projects_jira_active] == -1
+            results[:projects_jira_active] = count(services)
+          else
+            results[:projects_jira_active] += count(services)
+          end
+        end
+
+        results
       end
 
       def user_preferences_usage
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index b2d6ada91fa54be2f100ed70c0d9a649b71cc51b..c4cb3f07fe74de9511b659ca0a0a549a0f642b61 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -9,11 +9,7 @@
   factory :custom_issue_tracker_service, class: CustomIssueTrackerService do
     project
     active true
-    properties(
-      project_url: 'https://project.url.com',
-      issues_url: 'https://issues.url.com',
-      new_issue_url: 'https://newissue.url.com'
-    )
+    issue_tracker
   end
 
   factory :emails_on_push_service do
@@ -47,12 +43,24 @@
   factory :jira_service do
     project
     active true
-    properties(
-      url: 'https://jira.example.com',
-      username: 'jira_user',
-      password: 'my-secret-password',
-      project_key: 'jira-key'
-    )
+
+    transient do
+      create_data true
+      url 'https://jira.example.com'
+      api_url nil
+      username 'jira_username'
+      password 'jira_password'
+      jira_issue_transition_id '56-1'
+    end
+
+    after(:build) do |service, evaluator|
+      if evaluator.create_data
+        create(:jira_tracker_data, service: service,
+               url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id,
+               username: evaluator.username, password: evaluator.password
+        )
+      end
+    end
   end
 
   factory :bugzilla_service do
@@ -80,20 +88,26 @@
   end
 
   trait :issue_tracker do
-    properties(
-      project_url: 'http://issue-tracker.example.com',
-      issues_url: 'http://issue-tracker.example.com/issues/:id',
-      new_issue_url: 'http://issue-tracker.example.com'
-    )
+    transient do
+      create_data true
+      project_url 'http://issuetracker.example.com'
+      issues_url 'http://issues.example.com/issues/:id'
+      new_issue_url 'http://new-issue.example.com'
+    end
+
+    after(:build) do |service, evaluator|
+      if evaluator.create_data
+        create(:issue_tracker_data, service: service,
+               project_url: evaluator.project_url, issues_url: evaluator.issues_url, new_issue_url: evaluator.new_issue_url
+        )
+      end
+    end
   end
 
   trait :jira_cloud_service do
-    properties(
-      url: 'https://mysite.atlassian.net',
-      username: 'jira_user',
-      password: 'my-secret-password',
-      project_key: 'jira-key'
-    )
+    url 'https://mysite.atlassian.net'
+    username 'jira_user'
+    password 'my-secret-password'
   end
 
   factory :hipchat_service do
@@ -102,15 +116,21 @@
     token 'test_token'
   end
 
+  # this is for testing storing values inside properties, which is deprecated and will be removed in
+  # https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
   trait :without_properties_callback do
+    jira_tracker_data nil
+    issue_tracker_data nil
+    create_data false
+
     after(:build) do |service|
-      allow(service).to receive(:handle_properties)
+      IssueTrackerService.skip_callback(:validation, :before, :handle_properties)
     end
 
-    after(:create) do |service|
-      # we have to remove the stub because the behaviour of
-      # handle_properties method is tested after the creation
-      allow(service).to receive(:handle_properties).and_call_original
+    to_create { |instance| instance.save(validate: false)}
+
+    after(:create) do
+      IssueTrackerService.set_callback(:validation, :before, :handle_properties)
     end
   end
 end
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
index 7eb63fea41349d23304b6c69943d2aa5a4d81acc..47ea273ef3afb0e6495c6543eaba5bb01ac6d907 100644
--- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -34,7 +34,7 @@
         result = described_class.call(markdown, project: project)[:output]
         link = result.css('a').first
 
-        expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12'
+        expect(link['href']).to eq 'http://issues.example.com/issues/12'
       end
 
       it 'parses cross-project references to regular issues' do
@@ -63,7 +63,7 @@
         result = described_class.call(markdown, project: project)[:output]
         link = result.css('a').first
 
-        expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12'
+        expect(link['href']).to eq 'http://issues.example.com/issues/12'
       end
 
       it 'allows to use long external reference syntax for Redmine' do
@@ -72,7 +72,7 @@
         result = described_class.call(markdown, project: project)[:output]
         link = result.css('a').first
 
-        expect(link['href']).to eq 'http://issue-tracker.example.com/issues/12'
+        expect(link['href']).to eq 'http://issues.example.com/issues/12'
       end
 
       it 'parses cross-project references to regular issues' do
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 8eb64b97d6a863405bddc92ad6ae15a6004f9359..62787c5abafa3f156f384671e7f2d02b744def1f 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -3,14 +3,16 @@
 require 'spec_helper'
 
 describe Gitlab::UsageData do
-  let(:projects) { create_list(:project, 3) }
+  let(:projects) { create_list(:project, 4) }
   let!(:board) { create(:board, project: projects[0]) }
 
   describe '#data' do
     before do
       create(:jira_service, project: projects[0])
-      create(:jira_service, project: projects[1])
+      create(:jira_service, :without_properties_callback, project: projects[1])
       create(:jira_service, :jira_cloud_service, project: projects[2])
+      create(:jira_service, :without_properties_callback, project: projects[3],
+             properties: { url: 'https://mysite.atlassian.net' })
       create(:prometheus_service, project: projects[1])
       create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
       create(:service, project: projects[1], type: 'SlackService', active: true)
@@ -156,7 +158,7 @@
       count_data = subject[:counts]
 
       expect(count_data[:boards]).to eq(1)
-      expect(count_data[:projects]).to eq(3)
+      expect(count_data[:projects]).to eq(4)
       expect(count_data.keys).to include(*expected_keys)
       expect(expected_keys - count_data.keys).to be_empty
     end
@@ -164,14 +166,14 @@
     it 'gathers projects data correctly' do
       count_data = subject[:counts]
 
-      expect(count_data[:projects]).to eq(3)
+      expect(count_data[:projects]).to eq(4)
       expect(count_data[:projects_prometheus_active]).to eq(1)
-      expect(count_data[:projects_jira_active]).to eq(3)
+      expect(count_data[:projects_jira_active]).to eq(4)
       expect(count_data[:projects_jira_server_active]).to eq(2)
-      expect(count_data[:projects_jira_cloud_active]).to eq(1)
+      expect(count_data[:projects_jira_cloud_active]).to eq(2)
       expect(count_data[:projects_slack_notifications_active]).to eq(2)
       expect(count_data[:projects_slack_slash_active]).to eq(1)
-      expect(count_data[:projects_with_repositories_enabled]).to eq(2)
+      expect(count_data[:projects_with_repositories_enabled]).to eq(3)
       expect(count_data[:projects_with_error_tracking_enabled]).to eq(1)
 
       expect(count_data[:clusters_enabled]).to eq(7)
diff --git a/spec/models/project_services/bugzilla_service_spec.rb b/spec/models/project_services/bugzilla_service_spec.rb
index 74c85a13c88e7e25997b6bd8d6d6a07f537a0d26..e25d87f61d659755ac62fd5cfb1976a2d0807555 100644
--- a/spec/models/project_services/bugzilla_service_spec.rb
+++ b/spec/models/project_services/bugzilla_service_spec.rb
@@ -48,7 +48,7 @@
         create(:bugzilla_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in separated fields' do
@@ -56,7 +56,7 @@
         create(:bugzilla_service, title: title, description: description, properties: access_params)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
@@ -65,7 +65,7 @@
         create(:bugzilla_service, :without_properties_callback, title: title, description: description, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when no title & description are set' do
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 5259357a2540d8eb63b51894d853762085c07baa..8359bc6807adf9e5d7a03472dcd88398dbe179d0 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -62,7 +62,7 @@
         create(:custom_issue_tracker_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in separated fields' do
@@ -70,7 +70,7 @@
         create(:custom_issue_tracker_service, title: title, description: description, properties: access_params)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
@@ -79,7 +79,7 @@
         create(:custom_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when no title & description are set' do
diff --git a/spec/models/project_services/data_fields_spec.rb b/spec/models/project_services/data_fields_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..146db0ae2278e5f7e11cefc3483ddfa9020dbb4e
--- /dev/null
+++ b/spec/models/project_services/data_fields_spec.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DataFields do
+  let(:url) { 'http://url.com' }
+  let(:username) { 'username_one' }
+  let(:properties) do
+    { url: url, username: username }
+  end
+
+  shared_examples 'data fields' do
+    describe '#arg' do
+      it 'returns an argument correctly' do
+        expect(service.url).to eq(url)
+      end
+    end
+
+    describe '{arg}_changed?' do
+      it 'returns false when the property has not been assigned a new value' do
+        service.username = 'new_username'
+        service.validate
+        expect(service.url_changed?).to be_falsy
+      end
+
+      it 'returns true when the property has been assigned a different value' do
+        service.url = "http://example.com"
+        service.validate
+        expect(service.url_changed?).to be_truthy
+      end
+
+      it 'returns true when the property has been assigned a different value twice' do
+        service.url = "http://example.com"
+        service.url = "http://example.com"
+        service.validate
+        expect(service.url_changed?).to be_truthy
+      end
+
+      it 'returns false when the property has been re-assigned the same value' do
+        service.url = 'http://url.com'
+        service.validate
+        expect(service.url_changed?).to be_falsy
+      end
+    end
+
+    describe '{arg}_touched?' do
+      it 'returns false when the property has not been assigned a new value' do
+        service.username = 'new_username'
+        service.validate
+        expect(service.url_changed?).to be_falsy
+      end
+
+      it 'returns true when the property has been assigned a different value' do
+        service.url = "http://example.com"
+        service.validate
+        expect(service.url_changed?).to be_truthy
+      end
+
+      it 'returns true when the property has been assigned a different value twice' do
+        service.url = "http://example.com"
+        service.url = "http://example.com"
+        service.validate
+        expect(service.url_changed?).to be_truthy
+      end
+
+      it 'returns true when the property has been re-assigned the same value' do
+        service.url = 'http://url.com'
+        expect(service.url_touched?).to be_truthy
+      end
+
+      it 'returns false when the property has been re-assigned the same value' do
+        service.url = 'http://url.com'
+        service.validate
+        expect(service.url_changed?).to be_falsy
+      end
+    end
+  end
+
+  context 'when data are stored in data_fields' do
+    let(:service) do
+      create(:jira_service, url: url, username: username)
+    end
+
+    it_behaves_like 'data fields'
+
+    describe '{arg}_was?' do
+      it 'returns nil' do
+        service.url = 'http://example.com'
+        service.validate
+        expect(service.url_was).to be_nil
+      end
+    end
+  end
+
+  context 'when data are stored in properties' do
+    let(:service) { create(:jira_service, :without_properties_callback, properties: properties) }
+
+    it_behaves_like 'data fields'
+
+    describe '{arg}_was?' do
+      it 'returns nil when the property has not been assigned a new value' do
+        service.username = 'new_username'
+        service.validate
+        expect(service.url_was).to be_nil
+      end
+
+      it 'returns initial value when the property has been assigned a different value' do
+        service.url = 'http://example.com'
+        service.validate
+        expect(service.url_was).to eq('http://url.com')
+      end
+
+      it 'returns initial value when the property has been re-assigned the same value' do
+        service.url = 'http://url.com'
+        service.validate
+        expect(service.url_was).to eq('http://url.com')
+      end
+    end
+  end
+
+  context 'when data are stored in both properties and data_fields' do
+    let(:service) do
+      create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
+        create(:jira_tracker_data, properties.merge(service: service))
+      end
+    end
+
+    it_behaves_like 'data fields'
+
+    describe '{arg}_was?' do
+      it 'returns nil' do
+        service.url = 'http://example.com'
+        service.validate
+        expect(service.url_was).to be_nil
+      end
+    end
+  end
+end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 0c4fc290a13001d5424087f0089ce6bc27b1b374..4f3736ca65b3f622a6e10cddd887d1d207c77093 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -65,7 +65,7 @@
         create(:gitlab_issue_tracker_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in separated fields' do
@@ -73,7 +73,7 @@
         create(:gitlab_issue_tracker_service, title: title, description: description, properties: access_params)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
@@ -82,7 +82,7 @@
         create(:gitlab_issue_tracker_service, :without_properties_callback, title: title, description: description, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when no title & description are set' do
diff --git a/spec/models/project_services/issue_tracker_data_spec.rb b/spec/models/project_services/issue_tracker_data_spec.rb
index aaab654f874caf7bbddef3d714e8c0dc2eb95f75..db617cf0abbb1cc3f8f50da4bb96a2c57d9f9ee3 100644
--- a/spec/models/project_services/issue_tracker_data_spec.rb
+++ b/spec/models/project_services/issue_tracker_data_spec.rb
@@ -8,28 +8,4 @@
   describe 'Associations' do
     it { is_expected.to belong_to :service }
   end
-
-  describe 'Validations' do
-    subject { described_class.new(service: service) }
-
-    context 'url validations' do
-      context 'when service is inactive' do
-        it { is_expected.not_to validate_presence_of(:project_url) }
-        it { is_expected.not_to validate_presence_of(:issues_url) }
-      end
-
-      context 'when service is active' do
-        before do
-          service.update(active: true)
-        end
-
-        it_behaves_like 'issue tracker service URL attribute', :project_url
-        it_behaves_like 'issue tracker service URL attribute', :issues_url
-        it_behaves_like 'issue tracker service URL attribute', :new_issue_url
-
-        it { is_expected.to validate_presence_of(:project_url) }
-        it { is_expected.to validate_presence_of(:issues_url) }
-      end
-    end
-  end
 end
diff --git a/spec/models/project_services/issue_tracker_service_spec.rb b/spec/models/project_services/issue_tracker_service_spec.rb
index 2fc4d69c2db168cb2228d568d3ab26102a2fec29..f1cdee5c4a3c1fcb101ba1e971992dfb66927edb 100644
--- a/spec/models/project_services/issue_tracker_service_spec.rb
+++ b/spec/models/project_services/issue_tracker_service_spec.rb
@@ -7,7 +7,7 @@
     let(:project) { create :project }
 
     describe 'only one issue tracker per project' do
-      let(:service) { RedmineService.new(project: project, active: true) }
+      let(:service) { RedmineService.new(project: project, active: true, issue_tracker_data: build(:issue_tracker_data)) }
 
       before do
         create(:custom_issue_tracker_service, project: project)
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 02060699e9a3a8ff3674bf21c1f2c294eb7dfd14..a976745023bb44c80dc2fe1cdfe51238f118346e 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -6,10 +6,18 @@
   include Gitlab::Routing
   include AssetsHelpers
 
+  let(:title) { 'custom title' }
+  let(:description) { 'custom description' }
+  let(:url) { 'http://jira.example.com' }
+  let(:api_url) { 'http://api-jira.example.com' }
+  let(:username) { 'jira-username' }
+  let(:password) { 'jira-password' }
+  let(:transition_id) { 'test27' }
+
   describe '#options' do
     let(:service) do
-      described_class.new(
-        project: build_stubbed(:project),
+      described_class.create(
+        project: create(:project),
         active: true,
         username: 'username',
         password: 'test',
@@ -32,78 +40,6 @@
   describe 'Associations' do
     it { is_expected.to belong_to :project }
     it { is_expected.to have_one :service_hook }
-    it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) }
-    it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) }
-    it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) }
-    it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) }
-  end
-
-  describe 'Validations' do
-    context 'when service is active' do
-      before do
-        subject.active = true
-      end
-
-      it { is_expected.to validate_presence_of(:url) }
-      it_behaves_like 'issue tracker service URL attribute', :url
-    end
-
-    context 'when service is inactive' do
-      before do
-        subject.active = false
-      end
-
-      it { is_expected.not_to validate_presence_of(:url) }
-      it { is_expected.not_to validate_presence_of(:username) }
-      it { is_expected.not_to validate_presence_of(:password) }
-    end
-
-    context 'validating urls' do
-      let(:service) do
-        described_class.new(
-          project: create(:project),
-          active: true,
-          username: 'username',
-          password: 'test',
-          jira_issue_transition_id: 24,
-          url: 'http://jira.test.com'
-        )
-      end
-
-      it 'is valid when all fields have required values' do
-        expect(service).to be_valid
-      end
-
-      it 'is not valid when url is not a valid url' do
-        service.url = 'not valid'
-
-        expect(service).not_to be_valid
-      end
-
-      it 'is not valid when api url is not a valid url' do
-        service.api_url = 'not valid'
-
-        expect(service).not_to be_valid
-      end
-
-      it 'is not valid when username is missing' do
-        service.username = nil
-
-        expect(service).not_to be_valid
-      end
-
-      it 'is not valid when password is missing' do
-        service.password = nil
-
-        expect(service).not_to be_valid
-      end
-
-      it 'is valid when api url is a valid url' do
-        service.api_url = 'http://jira.test.com/api'
-
-        expect(service).to be_valid
-      end
-    end
   end
 
   describe '.reference_pattern' do
@@ -118,55 +54,260 @@
   describe '#create' do
     let(:params) do
       {
-        project: create(:project), title: 'custom title', description: 'custom description'
+        project: create(:project),
+        title: 'custom title', description: 'custom description',
+        url: url, api_url: api_url,
+        username: username, password: password,
+        jira_issue_transition_id: transition_id
       }
     end
 
     subject { described_class.create(params) }
 
-    it 'does not store title & description into properties' do
-      expect(subject.properties.keys).not_to include('title', 'description')
+    it 'does not store data into properties' do
+      expect(subject.properties).to be_nil
+    end
+
+    it 'sets title  correctly' do
+      service = subject
+
+      expect(service.title).to eq('custom title')
     end
 
-    it 'sets title & description correctly' do
+    it 'sets service data correctly' do
       service = subject
 
       expect(service.title).to eq('custom title')
       expect(service.description).to eq('custom description')
     end
+
+    it 'stores data in data_fields correcty' do
+      service = subject
+
+      expect(service.jira_tracker_data.url).to eq(url)
+      expect(service.jira_tracker_data.api_url).to eq(api_url)
+      expect(service.jira_tracker_data.username).to eq(username)
+      expect(service.jira_tracker_data.password).to eq(password)
+      expect(service.jira_tracker_data.jira_issue_transition_id).to eq(transition_id)
+    end
   end
 
+  # we need to make sure we are able to read both from properties and jira_tracker_data table
+  # TODO: change this as part of #63084
   context 'overriding properties' do
-    let(:url) { 'http://issue_tracker.example.com' }
     let(:access_params) do
-      { url: url, username: 'username', password: 'password' }
+      { url: url, api_url: api_url, username: username, password: password,
+        jira_issue_transition_id: transition_id }
+    end
+    let(:data_params) do
+      {
+        url: url, api_url: api_url,
+        username: username, password: password,
+        jira_issue_transition_id: transition_id
+      }
+    end
+
+    shared_examples 'handles jira fields' do
+      let(:data_params) do
+        {
+          url: url, api_url: api_url,
+          username: username, password: password,
+          jira_issue_transition_id: transition_id
+        }
+      end
+
+      context 'reading data' do
+        it 'reads data correctly' do
+          expect(service.url).to eq(url)
+          expect(service.api_url).to eq(api_url)
+          expect(service.username).to eq(username)
+          expect(service.password).to eq(password)
+          expect(service.jira_issue_transition_id).to eq(transition_id)
+        end
+      end
+
+      context '#update' do
+        context 'basic update' do
+          let(:new_username) { 'new_username' }
+          let(:new_url) { 'http://jira-new.example.com' }
+
+          before do
+            service.update(username: new_username, url: new_url)
+          end
+
+          it 'leaves properties field emtpy' do
+            # expect(service.reload.properties).to be_empty
+          end
+
+          it 'stores updated data in jira_tracker_data table' do
+            data = service.jira_tracker_data.reload
+
+            expect(data.url).to eq(new_url)
+            expect(data.api_url).to eq(api_url)
+            expect(data.username).to eq(new_username)
+            expect(data.password).to eq(password)
+            expect(data.jira_issue_transition_id).to eq(transition_id)
+          end
+        end
+
+        context 'stored password invalidation' do
+          context 'when a password was previously set' do
+            context 'when only web url present' do
+              let(:data_params) do
+                {
+                  url: url, api_url: nil,
+                  username: username, password: password,
+                  jira_issue_transition_id: transition_id
+                }
+              end
+
+              it 'resets password if url changed' do
+                service
+                service.url = 'http://jira_edited.example.com'
+                service.save
+
+                expect(service.reload.url).to eq('http://jira_edited.example.com')
+                expect(service.password).to be_nil
+              end
+
+              it 'does not reset password if url "changed" to the same url as before' do
+                service.title = 'aaaaaa'
+                service.url = 'http://jira.example.com'
+                service.save
+
+                expect(service.reload.url).to eq('http://jira.example.com')
+                expect(service.password).not_to be_nil
+              end
+
+              it 'resets password if url not changed but api url added' do
+                service.api_url = 'http://jira_edited.example.com/rest/api/2'
+                service.save
+
+                expect(service.reload.api_url).to eq('http://jira_edited.example.com/rest/api/2')
+                expect(service.password).to be_nil
+              end
+
+              it 'does not reset password if new url is set together with password, even if it\'s the same password' do
+                service.url = 'http://jira_edited.example.com'
+                service.password = password
+                service.save
+
+                expect(service.password).to eq(password)
+                expect(service.url).to eq('http://jira_edited.example.com')
+              end
+
+              it 'resets password if url changed, even if setter called multiple times' do
+                service.url = 'http://jira1.example.com/rest/api/2'
+                service.url = 'http://jira1.example.com/rest/api/2'
+                service.save
+
+                expect(service.password).to be_nil
+              end
+
+              it 'does not reset password if username changed' do
+                service.username = 'some_name'
+                service.save
+
+                expect(service.reload.password).to eq(password)
+              end
+
+              it 'does not reset password if password changed' do
+                service.url = 'http://jira_edited.example.com'
+                service.password = 'new_password'
+                service.save
+
+                expect(service.reload.password).to eq('new_password')
+              end
+
+              it 'does not reset password if the password is touched and same as before' do
+                service.url = 'http://jira_edited.example.com'
+                service.password = password
+                service.save
+
+                expect(service.reload.password).to eq(password)
+              end
+            end
+
+            context 'when both web and api url present' do
+              let(:data_params) do
+                {
+                  url: url, api_url: 'http://jira.example.com/rest/api/2',
+                  username: username, password: password,
+                  jira_issue_transition_id: transition_id
+                }
+              end
+
+              it 'resets password if api url changed' do
+                service.api_url = 'http://jira_edited.example.com/rest/api/2'
+                service.save
+
+                expect(service.password).to be_nil
+              end
+
+              it 'does not reset password if url changed' do
+                service.url = 'http://jira_edited.example.com'
+                service.save
+
+                expect(service.password).to eq(password)
+              end
+
+              it 'resets password if api url set to empty' do
+                service.update(api_url: '')
+
+                expect(service.reload.password).to be_nil
+              end
+            end
+          end
+
+          context 'when no password was previously set' do
+            let(:data_params) do
+              {
+                url: url, username: username
+              }
+            end
+
+            it 'saves password if new url is set together with password' do
+              service.url = 'http://jira_edited.example.com/rest/api/2'
+              service.password = 'password'
+              service.save
+              expect(service.reload.password).to eq('password')
+              expect(service.reload.url).to eq('http://jira_edited.example.com/rest/api/2')
+            end
+          end
+        end
+      end
     end
 
     # this  will be removed as part of https://gitlab.com/gitlab-org/gitlab-ce/issues/63084
     context 'when data are stored in properties' do
-      let(:properties) { access_params.merge(title: title, description: description) }
-      let(:service) do
+      let(:properties) { data_params.merge(title: title, description: description) }
+      let!(:service) do
         create(:jira_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
+      it_behaves_like 'handles jira fields'
     end
 
     context 'when data are stored in separated fields' do
       let(:service) do
-        create(:jira_service, title: title, description: description, properties: access_params)
+        create(:jira_service, data_params.merge(properties: {}, title: title, description: description))
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
+      it_behaves_like 'handles jira fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
-      let(:properties) { access_params.merge(title: 'wrong title', description: 'wrong description') }
+      let(:properties) { data_params.merge(title: title, description: description) }
       let(:service) do
-        create(:jira_service, :without_properties_callback, title: title, description: description, properties: properties)
+        create(:jira_service, :without_properties_callback, active: false, properties: properties).tap do |service|
+          create(:jira_tracker_data, data_params.merge(service: service))
+        end
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
+      it_behaves_like 'handles jira fields'
     end
 
     context 'when no title & description are set' do
@@ -410,111 +551,6 @@ def test_settings(api_url = nil)
     end
   end
 
-  describe 'Stored password invalidation' do
-    let(:project) { create(:project) }
-
-    context 'when a password was previously set' do
-      before do
-        @jira_service = described_class.create!(
-          project: project,
-          properties: {
-            url: 'http://jira.example.com/web',
-            username: 'mic',
-            password: 'password'
-          }
-        )
-      end
-
-      context 'when only web url present' do
-        it 'reset password if url changed' do
-          @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
-          @jira_service.save
-
-          expect(@jira_service.password).to be_nil
-        end
-
-        it 'reset password if url not changed but api url added' do
-          @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
-          @jira_service.save
-
-          expect(@jira_service.password).to be_nil
-        end
-      end
-
-      context 'when both web and api url present' do
-        before do
-          @jira_service.api_url = 'http://jira.example.com/rest/api/2'
-          @jira_service.password = 'password'
-
-          @jira_service.save
-        end
-        it 'reset password if api url changed' do
-          @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
-          @jira_service.save
-
-          expect(@jira_service.password).to be_nil
-        end
-
-        it 'does not reset password if url changed' do
-          @jira_service.url = 'http://jira_edited.example.com/rweb'
-          @jira_service.save
-
-          expect(@jira_service.password).to eq('password')
-        end
-
-        it 'reset password if api url set to empty' do
-          @jira_service.api_url = ''
-          @jira_service.save
-
-          expect(@jira_service.password).to be_nil
-        end
-      end
-
-      it 'does not reset password if username changed' do
-        @jira_service.username = 'some_name'
-        @jira_service.save
-
-        expect(@jira_service.password).to eq('password')
-      end
-
-      it 'does not reset password if new url is set together with password, even if it\'s the same password' do
-        @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
-        @jira_service.password = 'password'
-        @jira_service.save
-
-        expect(@jira_service.password).to eq('password')
-        expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
-      end
-
-      it 'resets password if url changed, even if setter called multiple times' do
-        @jira_service.url = 'http://jira1.example.com/rest/api/2'
-        @jira_service.url = 'http://jira1.example.com/rest/api/2'
-        @jira_service.save
-        expect(@jira_service.password).to be_nil
-      end
-    end
-
-    context 'when no password was previously set' do
-      before do
-        @jira_service = described_class.create(
-          project: project,
-          properties: {
-            url: 'http://jira.example.com/rest/api/2',
-            username: 'mic'
-          }
-        )
-      end
-
-      it 'saves password if new url is set together with password' do
-        @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
-        @jira_service.password = 'password'
-        @jira_service.save
-        expect(@jira_service.password).to eq('password')
-        expect(@jira_service.url).to eq('http://jira_edited.example.com/rest/api/2')
-      end
-    end
-  end
-
   describe 'description and title' do
     let(:title) { 'Jira One' }
     let(:description) { 'Jira One issue tracker' }
@@ -539,7 +575,7 @@ def test_settings(api_url = nil)
 
     context 'when it is set in properties' do
       it 'values from properties are returned' do
-        service = create(:jira_service, properties: properties)
+        service = create(:jira_service, :without_properties_callback, properties: properties)
 
         expect(service.title).to eq(title)
         expect(service.description).to eq(description)
@@ -602,8 +638,8 @@ def test_settings(api_url = nil)
         project = create(:project)
         service = project.create_jira_service(active: true)
 
-        expect(service.properties['url']).to eq('http://jira.sample/projects/project_a')
-        expect(service.properties['api_url']).to eq('http://jira.sample/api')
+        expect(service.url).to eq('http://jira.sample/projects/project_a')
+        expect(service.api_url).to eq('http://jira.sample/api')
       end
     end
 
diff --git a/spec/models/project_services/jira_tracker_data_spec.rb b/spec/models/project_services/jira_tracker_data_spec.rb
index 1b6ece8531b5b747155572f282315473dff93316..6cd3eb33d9be7d13917d853c57192c3bb8b7f76f 100644
--- a/spec/models/project_services/jira_tracker_data_spec.rb
+++ b/spec/models/project_services/jira_tracker_data_spec.rb
@@ -8,35 +8,4 @@
   describe 'Associations' do
     it { is_expected.to belong_to(:service) }
   end
-
-  describe 'Validations' do
-    subject { described_class.new(service: service) }
-
-    context 'jira_issue_transition_id' do
-      it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) }
-      it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) }
-      it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) }
-      it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) }
-    end
-
-    context 'url validations' do
-      context 'when service is inactive' do
-        it { is_expected.not_to validate_presence_of(:url) }
-        it { is_expected.not_to validate_presence_of(:username) }
-        it { is_expected.not_to validate_presence_of(:password) }
-      end
-
-      context 'when service is active' do
-        before do
-          service.update(active: true)
-        end
-
-        it_behaves_like 'issue tracker service URL attribute', :url
-
-        it { is_expected.to validate_presence_of(:url) }
-        it { is_expected.to validate_presence_of(:username) }
-        it { is_expected.to validate_presence_of(:password) }
-      end
-    end
-  end
 end
diff --git a/spec/models/project_services/redmine_service_spec.rb b/spec/models/project_services/redmine_service_spec.rb
index c1ee6546b12d735704c7834b777d10c3ade85ec4..4ef4064d0692d61e7760382b8089a641859a08b5 100644
--- a/spec/models/project_services/redmine_service_spec.rb
+++ b/spec/models/project_services/redmine_service_spec.rb
@@ -9,6 +9,15 @@
   end
 
   describe 'Validations' do
+    # if redmine is set in setting the urls are set to defaults
+    # therefore the validation passes as the values are not nil
+    before do
+      settings = {
+        'redmine' => {}
+      }
+      allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
+    end
+
     context 'when service is active' do
       before do
         subject.active = true
@@ -17,6 +26,7 @@
       it { is_expected.to validate_presence_of(:project_url) }
       it { is_expected.to validate_presence_of(:issues_url) }
       it { is_expected.to validate_presence_of(:new_issue_url) }
+
       it_behaves_like 'issue tracker service URL attribute', :project_url
       it_behaves_like 'issue tracker service URL attribute', :issues_url
       it_behaves_like 'issue tracker service URL attribute', :new_issue_url
@@ -54,7 +64,7 @@
         create(:redmine_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in separated fields' do
@@ -62,7 +72,7 @@
         create(:redmine_service, title: title, description: description, properties: access_params)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
@@ -71,7 +81,7 @@
         create(:redmine_service, :without_properties_callback, title: title, description: description, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when no title & description are set' do
diff --git a/spec/models/project_services/youtrack_service_spec.rb b/spec/models/project_services/youtrack_service_spec.rb
index c48bf487af0f9c4de61f61d9401e1df5dac45beb..eff9f451b1a2c8de78021b040d91e6ea4c1a639e 100644
--- a/spec/models/project_services/youtrack_service_spec.rb
+++ b/spec/models/project_services/youtrack_service_spec.rb
@@ -16,6 +16,7 @@
 
       it { is_expected.to validate_presence_of(:project_url) }
       it { is_expected.to validate_presence_of(:issues_url) }
+
       it_behaves_like 'issue tracker service URL attribute', :project_url
       it_behaves_like 'issue tracker service URL attribute', :issues_url
     end
@@ -51,7 +52,7 @@
         create(:youtrack_service, :without_properties_callback, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in separated fields' do
@@ -59,7 +60,7 @@
         create(:youtrack_service, title: title, description: description, properties: access_params)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when data are stored in both properties and separated fields' do
@@ -68,7 +69,7 @@
         create(:youtrack_service, :without_properties_callback, title: title, description: description, properties: properties)
       end
 
-      include_examples 'issue tracker fields'
+      it_behaves_like 'issue tracker fields'
     end
 
     context 'when no title & description are set' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0797b9a9d830863266ff011e7198f50bfd82a634..d96e139867791fa5fe555d40dce42e614a5a94ba 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -257,8 +257,8 @@
       expect(service.title).to eq('random title')
     end
 
-    it 'creates the properties' do
-      expect(service.properties).to eq({ "project_url" => "http://gitlab.example.com" })
+    it 'sets data correctly' do
+      expect(service.data_fields.project_url).to eq('http://gitlab.example.com')
     end
   end
 
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 76a70ab6e9e07787f1442936951a2a564c1208da..7153fcc99d7829a3ae361bda038d8c75230fb0c9 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -100,9 +100,15 @@
         expect(json_response['properties'].keys).to match_array(service_instance.api_field_names)
       end
 
-      it "returns empty hash if properties are empty" do
+      it "returns empty hash if properties and data fields are empty" do
         # deprecated services are not valid for update
         initialized_service.update_attribute(:properties, {})
+
+        if initialized_service.data_fields_present?
+          initialized_service.data_fields.destroy
+          initialized_service.reload
+        end
+
         get api("/projects/#{project.id}/services/#{dashed_service}", user)
 
         expect(response).to have_gitlab_http_status(200)
diff --git a/spec/support/helpers/jira_service_helper.rb b/spec/support/helpers/jira_service_helper.rb
index 57c33c81ea34867476e7eac2f4e2341746146ba1..c23a8d52c842df449ed1d1a9dbbefbfc7132d679 100644
--- a/spec/support/helpers/jira_service_helper.rb
+++ b/spec/support/helpers/jira_service_helper.rb
@@ -5,16 +5,16 @@ module JiraServiceHelper
   JIRA_API = JIRA_URL + "/rest/api/2"
 
   def jira_service_settings
-    properties = {
-      title: "Jira tracker",
-      url: JIRA_URL,
-      username: 'jira-user',
-      password: 'my-secret-password',
-      project_key: "JIRA",
-      jira_issue_transition_id: '1'
-    }
+    title = "Jira tracker"
+    url = JIRA_URL
+    username = 'jira-user'
+    password = 'my-secret-password'
+    jira_issue_transition_id = '1'
 
-    jira_tracker.update(properties: properties, active: true)
+    jira_tracker.update(
+      title: title, url: url, username: username, password: password,
+      jira_issue_transition_id: jira_issue_transition_id, active: true
+    )
   end
 
   def jira_issue_comments