diff --git a/common/djangoapps/student/emails.py b/common/djangoapps/student/emails.py
index a67739530ae8c769f9371048856104cb8c1559d1..6b4641722de6567bac843c731a37cd9a6e379755 100644
--- a/common/djangoapps/student/emails.py
+++ b/common/djangoapps/student/emails.py
@@ -22,7 +22,7 @@ def send_proctoring_requirements_email(context):
     user = context['user']
     try:
         msg = ProctoringRequirements(context=message_context).personalize(
-            recipient=Recipient(user.username, user.email),
+            recipient=Recipient(user.id, user.email),
             language=settings.LANGUAGE_CODE,
             user_context={'full_name': user.profile.name}
         )
diff --git a/common/djangoapps/student/forms.py b/common/djangoapps/student/forms.py
index 15f17dbc353958ae907d3dfb5381b65c283d0cd1..8e5a663dcdeec8e8c3a5ff6edaeb3e44fe6a4733 100644
--- a/common/djangoapps/student/forms.py
+++ b/common/djangoapps/student/forms.py
@@ -46,7 +46,7 @@ def send_account_recovery_email_for_user(user, request, email=None):
     })
 
     msg = AccountRecoveryMessage().personalize(
-        recipient=Recipient(user.username, email),
+        recipient=Recipient(user.id, email),
         language=get_user_preference(user, LANGUAGE_KEY),
         user_context=message_context,
     )
diff --git a/common/djangoapps/student/management/commands/recover_account.py b/common/djangoapps/student/management/commands/recover_account.py
index 55cb3ea6d87d154c1c415248abd75c9dd40c93f5..78e45cdbb3f67d32265917bf44dd7f9038321f68 100644
--- a/common/djangoapps/student/management/commands/recover_account.py
+++ b/common/djangoapps/student/management/commands/recover_account.py
@@ -124,7 +124,7 @@ class Command(BaseCommand):
 
         with emulate_http_request(site, user):
             msg = PasswordReset().personalize(
-                recipient=Recipient(user.username, email),
+                recipient=Recipient(user.id, email),
                 language=get_user_preference(user, LANGUAGE_KEY),
                 user_context=message_context,
             )
diff --git a/common/djangoapps/student/tasks.py b/common/djangoapps/student/tasks.py
index 1ab4a1f9d1985957f636df2bc1fe5fed6ad3551a..cc166dbd9ff7cf0bb3dbb35698a90fa22b68472b 100644
--- a/common/djangoapps/student/tasks.py
+++ b/common/djangoapps/student/tasks.py
@@ -40,7 +40,7 @@ def send_activation_email(self, msg_string, from_address=None):
     dest_addr = msg.recipient.email_address
 
     site = Site.objects.get_current()
-    user = User.objects.get(username=msg.recipient.username)
+    user = User.objects.get(id=msg.recipient.lms_user_id)
 
     try:
         with emulate_http_request(site=site, user=user):
diff --git a/common/djangoapps/student/tests/test_tasks.py b/common/djangoapps/student/tests/test_tasks.py
index b6cbbf76a2daf96ed695a8ec8acf9c0f80e81673..c0651bb9c7f46839b220983c15338db7719020b8 100644
--- a/common/djangoapps/student/tests/test_tasks.py
+++ b/common/djangoapps/student/tests/test_tasks.py
@@ -37,7 +37,7 @@ class SendActivationEmailTestCase(TestCase):
         assert 'platform_name' in self.msg.context
         assert 'contact_mailing_address' in self.msg.context
         # Verify the presence of the activation-email specific attributes
-        assert self.msg.recipient.username == self.student.username
+        assert self.msg.recipient.lms_user_id == self.student.id
         assert self.msg.recipient.email_address == self.student.email
         assert self.msg.context['routed_user'] == self.student.username
         assert self.msg.context['routed_user_email'] == self.student.email
diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py
index 5e5f4c1f2f3478261cb680c9f38a71d0213f1588..1ea658ff7d0cce2bfa8da638bb78127ed0372687 100644
--- a/common/djangoapps/student/views/management.py
+++ b/common/djangoapps/student/views/management.py
@@ -191,7 +191,7 @@ def compose_activation_email(root_url, user, user_registration=None, route_enabl
         dest_addr = user.email
 
     msg = AccountActivation().personalize(
-        recipient=Recipient(user.username, dest_addr),
+        recipient=Recipient(user.id, dest_addr),
         language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
         user_context=message_context,
     )
@@ -676,13 +676,13 @@ def do_email_change_request(user, new_email, activation_key=None, secondary_emai
 
     if secondary_email_change_request:
         msg = RecoveryEmailCreate().personalize(
-            recipient=Recipient(user.username, new_email),
+            recipient=Recipient(user.id, new_email),
             language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
             user_context=message_context,
         )
     else:
         msg = EmailChange().personalize(
-            recipient=Recipient(user.username, new_email),
+            recipient=Recipient(user.id, new_email),
             language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
             user_context=message_context,
         )
@@ -782,7 +782,7 @@ def confirm_email_change(request, key):
         })
 
         msg = EmailChangeConfirmation().personalize(
-            recipient=Recipient(user.username, user.email),
+            recipient=Recipient(user.id, user.email),
             language=preferences_api.get_user_preference(user, LANGUAGE_KEY),
             user_context=message_context,
         )
@@ -807,7 +807,7 @@ def confirm_email_change(request, key):
         user.save()
         pec.delete()
         # And send it to the new email...
-        msg.recipient = Recipient(user.username, pec.new_email)
+        msg.recipient = Recipient(user.id, pec.new_email)
         try:
             ace.send(msg)
         except Exception:  # pylint: disable=broad-except
diff --git a/lms/djangoapps/bulk_email/policies.py b/lms/djangoapps/bulk_email/policies.py
index b2294cdd3f0ab6d9513b07fb20b4c9327732d804..c108f416db950d263c3fe6d7194ce9645e4b530d 100644
--- a/lms/djangoapps/bulk_email/policies.py
+++ b/lms/djangoapps/bulk_email/policies.py
@@ -17,7 +17,7 @@ class CourseEmailOptout(Policy):  # lint-amnesty, pylint: disable=missing-class-
 
         # pylint: disable=line-too-long
         course_keys = [CourseKey.from_string(course_id) for course_id in course_ids]
-        if Optout.objects.filter(user__username=message.recipient.username, course_id__in=course_keys).count() == len(course_keys):
+        if Optout.objects.filter(user_id=message.recipient.lms_user_id, course_id__in=course_keys).count() == len(course_keys):
             return PolicyResult(deny={ChannelType.EMAIL})
 
         return PolicyResult(deny=frozenset())
diff --git a/lms/djangoapps/bulk_email/tests/test_course_optout.py b/lms/djangoapps/bulk_email/tests/test_course_optout.py
index 9e32863d5b2140cfaf4658899b20407e002490cf..df0dd9b0975106c629687d9ea3ebaad9fe80ccd5 100644
--- a/lms/djangoapps/bulk_email/tests/test_course_optout.py
+++ b/lms/djangoapps/bulk_email/tests/test_course_optout.py
@@ -186,7 +186,7 @@ class TestACEOptoutCourseEmails(ModuleStoreTestCase):
             app_label='foo',
             name='bar',
             recipient=Recipient(
-                username=self.student.username,
+                lms_user_id=self.student.id,
                 email_address=self.student.email,
             ),
             context={
diff --git a/lms/djangoapps/discussion/tasks.py b/lms/djangoapps/discussion/tasks.py
index 4f5d731445148978ffea00929da3236fc1b7c927..9c2013d6ae6351839c167217eee7088f5b14c04a 100644
--- a/lms/djangoapps/discussion/tasks.py
+++ b/lms/djangoapps/discussion/tasks.py
@@ -71,7 +71,7 @@ def send_ace_message(context):  # lint-amnesty, pylint: disable=missing-function
         with emulate_http_request(site=context['site'], user=thread_author):
             message_context = _build_message_context(context)
             message = ResponseNotification().personalize(
-                Recipient(thread_author.username, thread_author.email),
+                Recipient(thread_author.id, thread_author.email),
                 _get_course_language(context['course_id']),
                 message_context
             )
diff --git a/lms/djangoapps/discussion/tests/test_tasks.py b/lms/djangoapps/discussion/tests/test_tasks.py
index 7d54d177ad724c1847d22df29bab3eb3b0a2aadb..7430efb0bbbb7f02209aa1d63904ac82fe0633f8 100644
--- a/lms/djangoapps/discussion/tests/test_tasks.py
+++ b/lms/djangoapps/discussion/tests/test_tasks.py
@@ -216,7 +216,7 @@ class TaskTestCase(ModuleStoreTestCase):  # lint-amnesty, pylint: disable=missin
                 'site': site,
                 'site_id': site.id
             })
-            expected_recipient = Recipient(self.thread_author.username, self.thread_author.email)
+            expected_recipient = Recipient(self.thread_author.id, self.thread_author.email)
             actual_message = self.mock_ace_send.call_args_list[0][0][0]
             assert expected_message_context == actual_message.context
             assert expected_recipient == actual_message.recipient
diff --git a/lms/djangoapps/instructor/enrollment.py b/lms/djangoapps/instructor/enrollment.py
index 53bf7da3181c72085553ccce3752e2c01bf4d990..391a9a0328b6f8e791c906a917e00d3d75b42c40 100644
--- a/lms/djangoapps/instructor/enrollment.py
+++ b/lms/djangoapps/instructor/enrollment.py
@@ -495,7 +495,7 @@ def send_mail_to_student(student, param_dict, language=None):
 
     message_class = ace_emails_dict[message_type]
     message = message_class().personalize(
-        recipient=Recipient(username='', email_address=student),
+        recipient=Recipient(lms_user_id=0, email_address=student),
         language=language,
         user_context=param_dict,
     )
diff --git a/lms/djangoapps/verify_student/emails.py b/lms/djangoapps/verify_student/emails.py
index dcbad1b1b192b1c893fa522933fe98504506dccc..c568531ef7695357e21cfb7b4af59799070209ac 100644
--- a/lms/djangoapps/verify_student/emails.py
+++ b/lms/djangoapps/verify_student/emails.py
@@ -25,7 +25,7 @@ def send_verification_confirmation_email(context):
     try:
         with emulate_http_request(site=site, user=user):
             msg = VerificationSubmitted(context=message_context).personalize(
-                recipient=Recipient(user.username, user.email),
+                recipient=Recipient(user.id, user.email),
                 language=get_user_preference(user, LANGUAGE_KEY),
                 user_context={'full_name': user.profile.name}
             )
@@ -48,7 +48,7 @@ def send_verification_approved_email(context):
     try:
         with emulate_http_request(site=site, user=user):
             msg = VerificationApproved(context=message_context).personalize(
-                recipient=Recipient(user.username, user.email),
+                recipient=Recipient(user.id, user.email),
                 language=get_user_preference(user, LANGUAGE_KEY),
                 user_context={'full_name': user.profile.name}
             )
diff --git a/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py b/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
index 137af10a9a32751e85e04aa5b339bde6d8214b97..fe346ae2bebb2d5ae9b58d3b9c63ba4db440f673 100644
--- a/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
+++ b/lms/djangoapps/verify_student/management/commands/send_verification_expiry_email.py
@@ -213,7 +213,7 @@ class Command(BaseCommand):
                 user = users.get(pk=verification.user_id)
                 with emulate_http_request(site=site, user=user):
                     msg = expiry_email.personalize(
-                        recipient=Recipient(user.username, user.email),
+                        recipient=Recipient(user.id, user.email),
                         language=get_user_preference(user, LANGUAGE_KEY),
                         user_context={
                             'full_name': user.profile.name,
diff --git a/openedx/core/djangoapps/ace_common/tests/mixins.py b/openedx/core/djangoapps/ace_common/tests/mixins.py
index 7f9a7ddcc879f7eb999519ca0e8356f447ec3737..8f1bc132b7699ae151d6562bdd4dbb913f8be6e9 100644
--- a/openedx/core/djangoapps/ace_common/tests/mixins.py
+++ b/openedx/core/djangoapps/ace_common/tests/mixins.py
@@ -82,7 +82,7 @@ class EmailTemplateTagMixin(object):
         self.message = Message(
             app_label='test_app_label',
             name='test_name',
-            recipient=Recipient(username='test_user'),
+            recipient=Recipient(lms_user_id=123),
             context={},
             send_uuid=uuid.uuid4(),
         )
diff --git a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
index 31e33902ff663a4825737f35ea8952c734a5c956..8210b619cdaaf5660bd734a6635a79605e899e97 100644
--- a/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
+++ b/openedx/core/djangoapps/schedules/management/commands/tests/send_email_base.py
@@ -272,7 +272,7 @@ class ScheduleSendEmailTestMixin(FilteredQueryCountMixin):  # lint-amnesty, pyli
         }
         self._update_schedule_config(schedule_config_kwargs)
 
-        mock_message.from_string.return_value.recipient.username = user.username
+        mock_message.from_string.return_value.recipient.lms_user_id = user.id
         mock_msg = Mock()
         self.deliver_task(self.site_config.site.id, mock_msg)
         if is_enabled:
diff --git a/openedx/core/djangoapps/schedules/resolvers.py b/openedx/core/djangoapps/schedules/resolvers.py
index 492a67f30d69e2f628d0c61af0fff67994791af1..f2be31036e849de138b9fdd9a1b907fd02387671 100644
--- a/openedx/core/djangoapps/schedules/resolvers.py
+++ b/openedx/core/djangoapps/schedules/resolvers.py
@@ -88,7 +88,7 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
         for (user, language, context) in self.schedules_for_bin():
             msg = msg_type.personalize(
                 Recipient(
-                    user.username,
+                    user.id,
                     self.override_recipient_email or user.email,
                 ),
                 language,
@@ -370,7 +370,7 @@ class CourseUpdateResolver(BinnedSchedulesBaseResolver):
         for (user, language, context) in self.schedules_for_bin():
             msg = InstructorLedCourseUpdate().personalize(
                 Recipient(
-                    user.username,
+                    user.id,
                     self.override_recipient_email or user.email,
                 ),
                 language,
@@ -451,7 +451,7 @@ class CourseNextSectionUpdate(PrefixedDebugLoggerMixin, RecipientResolver):
         for (user, language, context) in schedules:
             msg = CourseUpdate().personalize(
                 Recipient(
-                    user.username,
+                    user.id,
                     self.override_recipient_email or user.email,
                 ),
                 language,
diff --git a/openedx/core/djangoapps/schedules/tasks.py b/openedx/core/djangoapps/schedules/tasks.py
index a885bece7b90dedd81db4030e2b08064b0ca7ac4..ac7fb3541505b4d359fb2805fcdcb62b4bb50dd2 100644
--- a/openedx/core/djangoapps/schedules/tasks.py
+++ b/openedx/core/djangoapps/schedules/tasks.py
@@ -277,7 +277,7 @@ def _schedule_send(msg_str, site_id, delivery_config_var, log_prefix):  # lint-a
     if _is_delivery_enabled(site, delivery_config_var, log_prefix):
         msg = Message.from_string(msg_str)
 
-        user = User.objects.get(username=msg.recipient.username)
+        user = User.objects.get(id=msg.recipient.lms_user_id)
         with emulate_http_request(site=site, user=user):
             _annonate_send_task_for_monitoring(msg)
             LOG.debug(u'%s: Sending message = %s', log_prefix, msg_str)
diff --git a/openedx/core/djangoapps/user_api/accounts/views.py b/openedx/core/djangoapps/user_api/accounts/views.py
index 00182aef7acffa38b0a3cc1854a62178abecfdde..54ad3cccc213cfa51f6cf323c8276d5dde5a2336 100644
--- a/openedx/core/djangoapps/user_api/accounts/views.py
+++ b/openedx/core/djangoapps/user_api/accounts/views.py
@@ -456,7 +456,7 @@ class DeactivateLogoutView(APIView):
                         default=settings.LANGUAGE_CODE
                     )
                     notification = DeletionNotificationMessage().personalize(
-                        recipient=Recipient(username='', email_address=user_email),
+                        recipient=Recipient(lms_user_id=0, email_address=user_email),
                         language=language_code,
                         user_context=notification_context,
                     )
diff --git a/openedx/core/djangoapps/user_authn/views/password_reset.py b/openedx/core/djangoapps/user_authn/views/password_reset.py
index 1f6b2dd42549110916d70bb2fb4d4f835838b9a9..cdbd0f057d4a1931ad6d008ff44158ff67958f5c 100644
--- a/openedx/core/djangoapps/user_authn/views/password_reset.py
+++ b/openedx/core/djangoapps/user_authn/views/password_reset.py
@@ -128,7 +128,7 @@ def send_password_reset_success_email(user, request):
     )
 
     msg = PasswordResetSuccess(context=message_context).personalize(
-        recipient=Recipient(user.username, user.email),
+        recipient=Recipient(user.id, user.email),
         language=user_language_preference,
         user_context={"name": user.profile.name},
     )
@@ -165,7 +165,7 @@ def send_password_reset_email_for_user(user, request, preferred_email=None):
     })
 
     msg = PasswordReset().personalize(
-        recipient=Recipient(user.username, preferred_email or user.email),
+        recipient=Recipient(user.id, preferred_email or user.email),
         language=user_language_preference,
         user_context=message_context,
     )
@@ -628,7 +628,7 @@ def password_change_request_handler(request):
                 })
 
                 msg = PasswordReset().personalize(
-                    recipient=Recipient(username='', email_address=email),
+                    recipient=Recipient(lms_user_id=0, email_address=email),
                     language=settings.LANGUAGE_CODE,
                     user_context=message_context,
                 )
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index 258e52c647111c1e911ee8f5d290f5329a10df30..79d9dfd3c79d44216ff600809b4acd28bf87ce2e 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -88,7 +88,7 @@ docopt==0.6.2             # via xmodule
 docutils==0.16            # via botocore
 drf-jwt==1.17.3           # via edx-drf-extensions
 drf-yasg==1.20.0          # via edx-api-doc-tools
-edx-ace==0.1.17           # via -r requirements/edx/base.in
+edx-ace==1.0.0            # via -r requirements/edx/base.in
 edx-analytics-data-api-client==0.17.0  # via -r requirements/edx/base.in
 edx-api-doc-tools==1.4.0  # via -r requirements/edx/base.in
 edx-bulk-grades==0.8.2    # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.in, staff-graded-xblock
diff --git a/requirements/edx/development.txt b/requirements/edx/development.txt
index 309c19417a91cb0d45445b632f9a2c182f5a9720..8965de0a4801842f2174787ea56365b6947d9a9a 100644
--- a/requirements/edx/development.txt
+++ b/requirements/edx/development.txt
@@ -99,7 +99,7 @@ docopt==0.6.2             # via -r requirements/edx/testing.txt, xmodule
 docutils==0.16            # via -r requirements/edx/testing.txt, botocore, m2r, sphinx
 drf-jwt==1.17.3           # via -r requirements/edx/testing.txt, edx-drf-extensions
 drf-yasg==1.20.0          # via -r requirements/edx/testing.txt, edx-api-doc-tools
-edx-ace==0.1.17           # via -r requirements/edx/testing.txt
+edx-ace==1.0.0            # via -r requirements/edx/testing.txt
 edx-analytics-data-api-client==0.17.0  # via -r requirements/edx/testing.txt
 edx-api-doc-tools==1.4.0  # via -r requirements/edx/testing.txt
 edx-bulk-grades==0.8.2    # via -c requirements/edx/../constraints.txt, -r requirements/edx/testing.txt, staff-graded-xblock
diff --git a/requirements/edx/testing.txt b/requirements/edx/testing.txt
index 71696dbd96dee9858f816074c598fe82b99a21c4..a4ebe7a34bfdd4e441cdeef717f7a6b1f0ab6f2b 100644
--- a/requirements/edx/testing.txt
+++ b/requirements/edx/testing.txt
@@ -96,7 +96,7 @@ docopt==0.6.2             # via -r requirements/edx/base.txt, xmodule
 docutils==0.16            # via -r requirements/edx/base.txt, botocore
 drf-jwt==1.17.3           # via -r requirements/edx/base.txt, edx-drf-extensions
 drf-yasg==1.20.0          # via -r requirements/edx/base.txt, edx-api-doc-tools
-edx-ace==0.1.17           # via -r requirements/edx/base.txt
+edx-ace==1.0.0            # via -r requirements/edx/base.txt
 edx-analytics-data-api-client==0.17.0  # via -r requirements/edx/base.txt
 edx-api-doc-tools==1.4.0  # via -r requirements/edx/base.txt
 edx-bulk-grades==0.8.2    # via -c requirements/edx/../constraints.txt, -r requirements/edx/base.txt, staff-graded-xblock