diff --git a/common/djangoapps/enrollment/data.py b/common/djangoapps/enrollment/data.py
index f58ac8c0e32c38626bbbac234a377490d2ae83e6..81d896d038f948e8f0a0d652f451605ca31ca650 100644
--- a/common/djangoapps/enrollment/data.py
+++ b/common/djangoapps/enrollment/data.py
@@ -1,22 +1,24 @@
 """
 Data Aggregation Layer of the Enrollment API. Collects all enrollment specific data into a single
 source to be used throughout the API.
-
 """
 import logging
+
 from django.contrib.auth.models import User
 from opaque_keys.edx.keys import CourseKey
-from xmodule.modulestore.django import modulestore
+
 from enrollment.errors import (
     CourseNotFoundError, CourseEnrollmentClosedError, CourseEnrollmentFullError,
     CourseEnrollmentExistsError, UserNotFoundError, InvalidEnrollmentAttribute
 )
 from enrollment.serializers import CourseEnrollmentSerializer, CourseField
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from student.models import (
     CourseEnrollment, NonExistentCourseError, EnrollmentClosedError,
     CourseFullError, AlreadyEnrolledError, CourseEnrollmentAttribute
 )
 
+
 log = logging.getLogger(__name__)
 
 
@@ -245,7 +247,7 @@ def _invalid_attribute(attributes):
 def get_course_enrollment_info(course_id, include_expired=False):
     """Returns all course enrollment information for the given course.
 
-    Based on the course id, return all related course information..
+    Based on the course id, return all related course information.
 
     Args:
         course_id (str): The course to retrieve enrollment information for.
@@ -261,9 +263,12 @@ def get_course_enrollment_info(course_id, include_expired=False):
 
     """
     course_key = CourseKey.from_string(course_id)
-    course = modulestore().get_course(course_key)
-    if course is None:
+
+    try:
+        course = CourseOverview.get_from_id(course_key)
+    except CourseOverview.DoesNotExist:
         msg = u"Requested enrollment information for unknown course {course}".format(course=course_id)
         log.warning(msg)
         raise CourseNotFoundError(msg)
-    return CourseField().to_native(course, include_expired=include_expired)
+    else:
+        return CourseField().to_native(course, include_expired=include_expired)
diff --git a/common/djangoapps/enrollment/serializers.py b/common/djangoapps/enrollment/serializers.py
index 4513b4ed955f6f07e9f5b65fcbf81e57098f738a..d9e6ce791834c8593981daaca003ec63390b7de7 100644
--- a/common/djangoapps/enrollment/serializers.py
+++ b/common/djangoapps/enrollment/serializers.py
@@ -1,12 +1,12 @@
 """
 Serializers for all Course Enrollment related return objects.
-
 """
 import logging
 
 from rest_framework import serializers
-from student.models import CourseEnrollment
+
 from course_modes.models import CourseMode
+from student.models import CourseEnrollment
 
 
 log = logging.getLogger(__name__)
@@ -39,19 +39,18 @@ class CourseField(serializers.RelatedField):
     """
 
     def to_native(self, course, **kwargs):
-        course_id = unicode(course.id)
         course_modes = ModeSerializer(
             CourseMode.modes_for_course(course.id, kwargs.get('include_expired', False), only_selectable=False)
         ).data  # pylint: disable=no-member
 
         return {
-            "course_id": course_id,
-            "enrollment_start": course.enrollment_start,
-            "enrollment_end": course.enrollment_end,
-            "course_start": course.start,
-            "course_end": course.end,
-            "invite_only": course.invitation_only,
-            "course_modes": course_modes,
+            'course_id': unicode(course.id),
+            'enrollment_start': course.enrollment_start,
+            'enrollment_end': course.enrollment_end,
+            'course_start': course.start,
+            'course_end': course.end,
+            'invite_only': course.invitation_only,
+            'course_modes': course_modes,
         }
 
 
diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py
index a648e1742c02ded502cd9fdaf1bc5ac23eb0e7f7..e8cd16b936a7037c2ea35f722e84fd98143cae44 100644
--- a/common/djangoapps/enrollment/tests/test_views.py
+++ b/common/djangoapps/enrollment/tests/test_views.py
@@ -16,7 +16,7 @@ from rest_framework.test import APITestCase
 from rest_framework import status
 from django.conf import settings
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
-from xmodule.modulestore.tests.factories import CourseFactory
+from xmodule.modulestore.tests.factories import CourseFactory, check_mongo_calls_range
 from django.test.utils import override_settings
 
 from course_modes.models import CourseMode
@@ -26,8 +26,9 @@ from util.models import RateLimitConfiguration
 from util.testing import UrlResetMixin
 from enrollment import api
 from enrollment.errors import CourseEnrollmentError
-from openedx.core.lib.django_test_client_utils import get_absolute_url
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
 from openedx.core.djangoapps.user_api.models import UserOrgTag
+from openedx.core.lib.django_test_client_utils import get_absolute_url
 from student.tests.factories import UserFactory, CourseModeFactory
 from student.models import CourseEnrollment
 from embargo.test_utils import restrict_course
@@ -47,6 +48,8 @@ class EnrollmentTestMixin(object):
             mode=CourseMode.HONOR,
             is_active=None,
             enrollment_attributes=None,
+            min_mongo_calls=0,
+            max_mongo_calls=0,
     ):
         """
         Enroll in the course and verify the response's status code. If the expected status is 200, also validates
@@ -77,31 +80,33 @@ class EnrollmentTestMixin(object):
         if as_server:
             extra['HTTP_X_EDX_API_KEY'] = self.API_KEY
 
-        with patch('enrollment.views.audit_log') as mock_audit_log:
-            url = reverse('courseenrollments')
-            response = self.client.post(url, json.dumps(data), content_type='application/json', **extra)
-            self.assertEqual(response.status_code, expected_status)
+        # Verify that the modulestore is queried as expected.
+        with check_mongo_calls_range(min_finds=min_mongo_calls, max_finds=max_mongo_calls):
+            with patch('enrollment.views.audit_log') as mock_audit_log:
+                url = reverse('courseenrollments')
+                response = self.client.post(url, json.dumps(data), content_type='application/json', **extra)
+                self.assertEqual(response.status_code, expected_status)
 
-            if expected_status == status.HTTP_200_OK:
-                data = json.loads(response.content)
-                self.assertEqual(course_id, data['course_details']['course_id'])
+                if expected_status == status.HTTP_200_OK:
+                    data = json.loads(response.content)
+                    self.assertEqual(course_id, data['course_details']['course_id'])
 
-                if mode is not None:
-                    self.assertEqual(mode, data['mode'])
+                    if mode is not None:
+                        self.assertEqual(mode, data['mode'])
 
-                if is_active is not None:
-                    self.assertEqual(is_active, data['is_active'])
-                else:
-                    self.assertTrue(data['is_active'])
+                    if is_active is not None:
+                        self.assertEqual(is_active, data['is_active'])
+                    else:
+                        self.assertTrue(data['is_active'])
 
-                if as_server:
-                    # Verify that an audit message was logged.
-                    self.assertTrue(mock_audit_log.called)
+                    if as_server:
+                        # Verify that an audit message was logged.
+                        self.assertTrue(mock_audit_log.called)
 
-                    # If multiple enrollment calls are made in the scope of a
-                    # single test, we want to validate that audit messages are
-                    # logged for each call.
-                    mock_audit_log.reset_mock()
+                        # If multiple enrollment calls are made in the scope of a
+                        # single test, we want to validate that audit messages are
+                        # logged for each call.
+                        mock_audit_log.reset_mock()
 
         return response
 
@@ -141,6 +146,10 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
         self.rate_limit, rate_duration = throttle.parse_rate(throttle.rate)
 
         self.course = CourseFactory.create()
+        # Load a CourseOverview. This initial load should result in a cache
+        # miss; the modulestore is queried and course metadata is cached.
+        __ = CourseOverview.get_from_id(self.course.id)
+
         self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
         self.other_user = UserFactory.create()
         self.client.login(username=self.USERNAME, password=self.PASSWORD)
@@ -382,6 +391,10 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
     @ddt.unpack
     def test_get_course_details_course_dates(self, start_datetime, end_datetime, expected_start, expected_end):
         course = CourseFactory.create(start=start_datetime, end=end_datetime)
+        # Load a CourseOverview. This initial load should result in a cache
+        # miss; the modulestore is queried and course metadata is cached.
+        __ = CourseOverview.get_from_id(course.id)
+
         self.assert_enrollment_status(course_id=unicode(course.id))
 
         # Check course details
@@ -411,7 +424,12 @@ class EnrollmentTest(EnrollmentTestMixin, ModuleStoreTestCase, APITestCase):
         self.assertEqual(data[0]['course_details']['course_end'], expected_end)
 
     def test_with_invalid_course_id(self):
-        self.assert_enrollment_status(course_id='entirely/fake/course', expected_status=status.HTTP_400_BAD_REQUEST)
+        self.assert_enrollment_status(
+            course_id='entirely/fake/course',
+            expected_status=status.HTTP_400_BAD_REQUEST,
+            min_mongo_calls=3,
+            max_mongo_calls=4
+        )
 
     def test_get_enrollment_details_bad_course(self):
         resp = self.client.get(
@@ -797,7 +815,12 @@ class EnrollmentEmbargoTest(EnrollmentTestMixin, UrlResetMixin, ModuleStoreTestC
     def setUp(self):
         """ Create a course and user, then log in. """
         super(EnrollmentEmbargoTest, self).setUp('embargo')
+
         self.course = CourseFactory.create()
+        # Load a CourseOverview. This initial load should result in a cache
+        # miss; the modulestore is queried and course metadata is cached.
+        __ = CourseOverview.get_from_id(self.course.id)
+
         self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
         self.client.login(username=self.USERNAME, password=self.PASSWORD)
         self.url = reverse('courseenrollments')
diff --git a/common/djangoapps/student/models.py b/common/djangoapps/student/models.py
index 686874ca9ed6ea914798b117e3aa6c09c89de495..ff9a863c3322e8602a8513cf43c42a0309e423cc 100644
--- a/common/djangoapps/student/models.py
+++ b/common/djangoapps/student/models.py
@@ -10,16 +10,19 @@ file and check it in at the same time as your model changes. To do that,
 2. ./manage.py lms schemamigration student --auto description_of_your_change
 3. Add the migration file created in edx-platform/common/djangoapps/student/migrations/
 """
+from collections import defaultdict, OrderedDict
 from datetime import datetime, timedelta
+from functools import total_ordering
 import hashlib
+from importlib import import_module
 import json
 import logging
 from pytz import UTC
-import uuid
-from collections import defaultdict, OrderedDict
-import dogstats_wrapper as dog_stats_api
 from urllib import urlencode
+import uuid
 
+import analytics
+from config_models.models import ConfigurationModel
 from django.utils.translation import ugettext_lazy as _
 from django.conf import settings
 from django.utils import timezone
@@ -33,28 +36,21 @@ from django.dispatch import receiver, Signal
 from django.core.exceptions import ObjectDoesNotExist
 from django.utils.translation import ugettext_noop
 from django_countries.fields import CountryField
-from config_models.models import ConfigurationModel
-from track import contexts
+import dogstats_wrapper as dog_stats_api
 from eventtracking import tracker
-from importlib import import_module
-
-from south.modelsinspector import add_introspection_rules
-
+from opaque_keys.edx.keys import CourseKey
 from opaque_keys.edx.locations import SlashSeparatedCourseKey
-
-import lms.lib.comment_client as cc
-from util.model_utils import emit_field_changed_events, get_changed_fields_dict
-from util.query import use_read_replica_if_available
+from south.modelsinspector import add_introspection_rules
+from track import contexts
 from xmodule_django.models import CourseKeyField, NoneToEmptyManager
-from xmodule.modulestore.exceptions import ItemNotFoundError
-from xmodule.modulestore.django import modulestore
-from opaque_keys.edx.keys import CourseKey
-from functools import total_ordering
 
 from certificates.models import GeneratedCertificate
 from course_modes.models import CourseMode
+import lms.lib.comment_client as cc
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
+from util.model_utils import emit_field_changed_events, get_changed_fields_dict
+from util.query import use_read_replica_if_available
 
-import analytics
 
 UNENROLL_DONE = Signal(providing_args=["course_enrollment", "skip_refund"])
 log = logging.getLogger(__name__)
@@ -1064,18 +1060,15 @@ class CourseEnrollment(models.Model):
         """
         # All the server-side checks for whether a user is allowed to enroll.
         try:
-            course = modulestore().get_course(course_key)
-        except ItemNotFoundError:
-            log.warning(
-                u"User %s failed to enroll in non-existent course %s",
-                user.username,
-                course_key.to_deprecated_string(),
-            )
-            raise NonExistentCourseError
+            course = CourseOverview.get_from_id(course_key)
+        except CourseOverview.DoesNotExist:
+            # This is here to preserve legacy behavior which allowed enrollment in courses
+            # announced before the start of content creation.
+            if check_access:
+                log.warning(u"User %s failed to enroll in non-existent course %s", user.username, unicode(course_key))
+                raise NonExistentCourseError
 
         if check_access:
-            if course is None:
-                raise NonExistentCourseError
             if CourseEnrollment.is_enrollment_closed(user, course):
                 log.warning(
                     u"User %s failed to enroll in course %s because enrollment is closed",
@@ -1320,7 +1313,8 @@ class CourseEnrollment(models.Model):
 
     @property
     def course(self):
-        return modulestore().get_course(self.course_id)
+        # Deprecated. Please use the `course_overview` property instead.
+        return self.course_overview
 
     @property
     def course_overview(self):
@@ -1334,7 +1328,6 @@ class CourseEnrollment(models.Model):
             become stale.
        """
         if not self._course_overview:
-            from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
             try:
                 self._course_overview = CourseOverview.get_from_id(self.course_id)
             except (CourseOverview.DoesNotExist, IOError):
diff --git a/common/djangoapps/student/tests/test_course_listing.py b/common/djangoapps/student/tests/test_course_listing.py
index cdf75a342b143a6ba8b6e3285dec9bcae9aa4bb4..362d85b95e8e3c25aff87e83df6413fbb974ec13 100644
--- a/common/djangoapps/student/tests/test_course_listing.py
+++ b/common/djangoapps/student/tests/test_course_listing.py
@@ -2,26 +2,27 @@
 Unit tests for getting the list of courses for a user through iterating all courses and
 by reversing group name formats.
 """
+import unittest
+
+from django.conf import settings
+from django.test.client import Client
 import mock
-from mock import patch, Mock
 
-from student.tests.factories import UserFactory
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
+from student.models import CourseEnrollment
 from student.roles import GlobalStaff
+from student.tests.factories import UserFactory
+from student.views import get_course_enrollments
+from xmodule.error_module import ErrorDescriptor
 from xmodule.modulestore import ModuleStoreEnum
+from xmodule.modulestore.django import modulestore
 from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
 from xmodule.modulestore.tests.factories import CourseFactory
-from xmodule.modulestore.django import modulestore
-from xmodule.error_module import ErrorDescriptor
-from django.test.client import Client
-from student.models import CourseEnrollment
-from student.views import get_course_enrollments
 from util.milestones_helpers import (
     get_pre_requisite_courses_not_completed,
     set_prerequisite_courses,
     seed_milestone_relationship_types
 )
-import unittest
-from django.conf import settings
 
 
 class TestCourseListing(ModuleStoreTestCase):
@@ -91,10 +92,12 @@ class TestCourseListing(ModuleStoreTestCase):
         course_key = mongo_store.make_course_key('Org1', 'Course1', 'Run1')
         self._create_course_with_access_groups(course_key, default_store=ModuleStoreEnum.Type.mongo)
 
-        with patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', Mock(side_effect=Exception)):
+        with mock.patch('xmodule.modulestore.mongo.base.MongoKeyValueStore', mock.Mock(side_effect=Exception)):
             self.assertIsInstance(modulestore().get_course(course_key), ErrorDescriptor)
 
-            # get courses through iterating all courses
+            # Invalidate (e.g., delete) the corresponding CourseOverview, forcing get_course to be called.
+            CourseOverview.objects.filter(id=course_key).delete()
+
             courses_list = list(get_course_enrollments(self.student, None, []))
             self.assertEqual(courses_list, [])
 
diff --git a/common/djangoapps/student/tests/test_recent_enrollments.py b/common/djangoapps/student/tests/test_recent_enrollments.py
index 07516af01a4e0d82c165cb36de597987416dec81..2fd54d87bfed1b3be3ebf1078c87ff0617a1910a 100644
--- a/common/djangoapps/student/tests/test_recent_enrollments.py
+++ b/common/djangoapps/student/tests/test_recent_enrollments.py
@@ -112,10 +112,10 @@ class TestRecentEnrollments(ModuleStoreTestCase):
         recent_course_list = _get_recently_enrolled_courses(courses_list)
         self.assertEqual(len(recent_course_list), 5)
 
-        self.assertEqual(recent_course_list[1].course, courses[0])
-        self.assertEqual(recent_course_list[2].course, courses[1])
-        self.assertEqual(recent_course_list[3].course, courses[2])
-        self.assertEqual(recent_course_list[4].course, courses[3])
+        self.assertEqual(recent_course_list[1].course.id, courses[0].id)
+        self.assertEqual(recent_course_list[2].course.id, courses[1].id)
+        self.assertEqual(recent_course_list[3].course.id, courses[2].id)
+        self.assertEqual(recent_course_list[4].course.id, courses[3].id)
 
     def test_dashboard_rendering(self):
         """
diff --git a/common/djangoapps/student/tests/tests.py b/common/djangoapps/student/tests/tests.py
index 00af8014b5f11e838b13745c8313af1319be53e1..42842cab37395aaa8490c3c5941fca0c382af49a 100644
--- a/common/djangoapps/student/tests/tests.py
+++ b/common/djangoapps/student/tests/tests.py
@@ -494,9 +494,9 @@ class DashboardTest(ModuleStoreTestCase):
         """
         Check that the student dashboard makes use of course metadata caching.
 
-        The first time the student dashboard displays a specific course, it will
-        make a call to the module store. After that first request, though, the
-        course's metadata should be cached as a CourseOverview.
+        After enrolling a student in a course, that course's metadata should be
+        cached as a CourseOverview. The student dashboard should never have to make
+        calls to the modulestore.
 
         Arguments:
             modulestore_type (ModuleStoreEnum.Type): Type of modulestore to create
@@ -511,23 +511,21 @@ class DashboardTest(ModuleStoreTestCase):
             involve adding fields to CourseOverview so that loading a full
             CourseDescriptor isn't necessary.
         """
-        # Create a course, log in the user, and enroll them in the course.
+        # Create a course and log in the user.
         test_course = CourseFactory.create(default_store=modulestore_type)
         self.client.login(username="jack", password="test")
-        CourseEnrollment.enroll(self.user, test_course.id)
 
-        # The first request will result in a modulestore query.
+        # Enrolling the user in the course will result in a modulestore query.
         with check_mongo_calls(expected_mongo_calls):
-            response_1 = self.client.get(reverse('dashboard'))
-            self.assertEquals(response_1.status_code, 200)
+            CourseEnrollment.enroll(self.user, test_course.id)
 
         # Subsequent requests will only result in SQL queries to load the
         # CourseOverview object that has been created.
         with check_mongo_calls(0):
+            response_1 = self.client.get(reverse('dashboard'))
+            self.assertEquals(response_1.status_code, 200)
             response_2 = self.client.get(reverse('dashboard'))
             self.assertEquals(response_2.status_code, 200)
-            response_3 = self.client.get(reverse('dashboard'))
-            self.assertEquals(response_3.status_code, 200)
 
     @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
     @patch.dict(settings.FEATURES, {"IS_EDX_DOMAIN": True})
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 853fd93036d9f852a17abd1d151cbf68e4da725e..3017f99d19900ec95eb53bb75af2c94c316b8e9c 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -1353,7 +1353,7 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
         otherwise a publish will be signalled at the end of the bulk operation
 
         Arguments:
-            library_updated - library_updated to which the signal applies
+            library_key - library_key to which the signal applies
         """
         signal_handler = getattr(self, 'signal_handler', None)
         if signal_handler:
@@ -1363,6 +1363,14 @@ class ModuleStoreWriteBase(ModuleStoreReadBase, ModuleStoreWrite):
             else:
                 signal_handler.send("library_updated", library_key=library_key)
 
+    def _emit_course_deleted_signal(self, course_key):
+        """
+        Helper method used to emit the course_deleted signal.
+        """
+        signal_handler = getattr(self, 'signal_handler', None)
+        if signal_handler:
+            signal_handler.send("course_deleted", course_key=course_key)
+
 
 def only_xmodules(identifier, entry_points):
     """Only use entry_points that are supplied by the xmodule package"""
diff --git a/common/lib/xmodule/xmodule/modulestore/django.py b/common/lib/xmodule/xmodule/modulestore/django.py
index 8c631a75aebd83b6cd90091520f38b53fd4b245e..8b91916b4590b30cdc4bd78bd224a653ef2f0f5c 100644
--- a/common/lib/xmodule/xmodule/modulestore/django.py
+++ b/common/lib/xmodule/xmodule/modulestore/django.py
@@ -87,11 +87,13 @@ class SignalHandler(object):
        do the actual work.
     """
     course_published = django.dispatch.Signal(providing_args=["course_key"])
+    course_deleted = django.dispatch.Signal(providing_args=["course_key"])
     library_updated = django.dispatch.Signal(providing_args=["library_key"])
 
     _mapping = {
         "course_published": course_published,
-        "library_updated": library_updated
+        "course_deleted": course_deleted,
+        "library_updated": library_updated,
     }
 
     def __init__(self, modulestore_class):
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
index 8c181b2639e77a61857bdfe09ccf4fd6e67dce9a..6e6335d3bdd2fab644ca050f5d2f6aca57a88a6c 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo/draft.py
@@ -167,6 +167,8 @@ class DraftModuleStore(MongoModuleStore):
         self.collection.remove(course_query, multi=True)
         self.delete_all_asset_metadata(course_key, user_id)
 
+        self._emit_course_deleted_signal(course_key)
+
     def clone_course(self, source_course_id, dest_course_id, user_id, fields=None, **kwargs):
         """
         Only called if cloning within this store or if env doesn't set up mixed.
diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
index f29a0b04a10a5a95edfe3a377bff5f9e6767acbe..3a829212c0ec3cce7fafe89ef5a1e18fe8ca2cbf 100644
--- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
+++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py
@@ -2443,6 +2443,8 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
         # in case the course is later restored.
         # super(SplitMongoModuleStore, self).delete_course(course_key, user_id)
 
+        self._emit_course_deleted_signal(course_key)
+
     @contract(block_map="dict(BlockKey: dict)", block_key=BlockKey)
     def inherit_settings(
         self, block_map, block_key, inherited_settings_map, inheriting_settings=None, inherited_from=None
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
index 1872a481d1f8bdb8673fac66fed41619f2ed5c37..74aa23066b78cce8b39ac5286e84fb17f3de754e 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mixed_modulestore.py
@@ -2414,6 +2414,34 @@ class TestMixedModuleStore(CommonMixedModuleStoreSetup):
                         self.assertEqual(receiver.call_count, 0)
                     self.assertEqual(receiver.call_count, 0)
 
+    @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
+    def test_course_deleted_signal(self, default):
+        with MongoContentstoreBuilder().build() as contentstore:
+            self.store = MixedModuleStore(
+                contentstore=contentstore,
+                create_modulestore_instance=create_modulestore_instance,
+                mappings={},
+                signal_handler=SignalHandler(MixedModuleStore),
+                **self.OPTIONS
+            )
+            self.addCleanup(self.store.close_all_connections)
+
+            with self.store.default_store(default):
+                self.assertIsNotNone(self.store.thread_cache.default_store.signal_handler)
+
+                with mock_signal_receiver(SignalHandler.course_deleted) as receiver:
+                    self.assertEqual(receiver.call_count, 0)
+
+                    # Create a course
+                    course = self.store.create_course('org_x', 'course_y', 'run_z', self.user_id)
+                    course_key = course.id
+
+                    # Delete the course
+                    course = self.store.delete_course(course_key, self.user_id)
+
+                    # Verify that the signal was emitted
+                    self.assertEqual(receiver.call_count, 1)
+
 
 @ddt.ddt
 @attr('mongo')
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index a4735f9d13d4543dad23b34d3725a55ef4426861..e544afab5ed6de9320183d5e7d4fc20a9aba9964 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -10,8 +10,8 @@ Note: The access control logic in this file does NOT check for enrollment in
   If enrollment is to be checked, use get_course_with_access in courseware.courses.
   It is a wrapper around has_access that additionally checks for enrollment.
 """
-import logging
 from datetime import datetime, timedelta
+import logging
 import pytz
 
 from django.conf import settings
@@ -245,6 +245,58 @@ def _can_load_course_on_mobile(user, course):
     )
 
 
+def _can_enroll_courselike(user, courselike):
+    """
+    Ascertain if the user can enroll in the given courselike object.
+
+    Arguments:
+        user (User): The user attempting to enroll.
+        courselike (CourseDescriptor or CourseOverview): The object representing the
+            course in which the user is trying to enroll.
+
+    Returns:
+        AccessResponse, indicating whether the user can enroll.
+    """
+    enrollment_domain = courselike.enrollment_domain
+    # Courselike objects (e.g., course descriptors and CourseOverviews) have an attribute named `id`
+    # which actually points to a CourseKey. Sigh.
+    course_key = courselike.id
+
+    # If using a registration method to restrict enrollment (e.g., Shibboleth)
+    if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and enrollment_domain:
+        if user is not None and user.is_authenticated() and \
+                ExternalAuthMap.objects.filter(user=user, external_domain=enrollment_domain):
+            debug("Allow: external_auth of " + enrollment_domain)
+            reg_method_ok = True
+        else:
+            reg_method_ok = False
+    else:
+        reg_method_ok = True
+
+    # If the user appears in CourseEnrollmentAllowed paired with the given course key,
+    # they may enroll. Note that as dictated by the legacy database schema, the filter
+    # call includes a `course_id` kwarg which requires a CourseKey.
+    if user is not None and user.is_authenticated():
+        if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course_key):
+            return ACCESS_GRANTED
+
+    if _has_staff_access_to_descriptor(user, courselike, course_key):
+        return ACCESS_GRANTED
+
+    if courselike.invitation_only:
+        debug("Deny: invitation only")
+        return ACCESS_DENIED
+
+    now = datetime.now(UTC())
+    enrollment_start = courselike.enrollment_start or datetime.min.replace(tzinfo=pytz.UTC)
+    enrollment_end = courselike.enrollment_end or datetime.max.replace(tzinfo=pytz.UTC)
+    if reg_method_ok and enrollment_start < now < enrollment_end:
+        debug("Allow: in enrollment period")
+        return ACCESS_GRANTED
+
+    return ACCESS_DENIED
+
+
 def _has_access_course_desc(user, action, course):
     """
     Check if user has access to a course descriptor.
@@ -271,54 +323,7 @@ def _has_access_course_desc(user, action, course):
         return _has_access_descriptor(user, 'load', course, course.id)
 
     def can_enroll():
-        """
-        First check if restriction of enrollment by login method is enabled, both
-            globally and by the course.
-        If it is, then the user must pass the criterion set by the course, e.g. that ExternalAuthMap
-            was set by 'shib:https://idp.stanford.edu/", in addition to requirements below.
-        Rest of requirements:
-        (CourseEnrollmentAllowed always overrides)
-          or
-        (staff can always enroll)
-          or
-        Enrollment can only happen in the course enrollment period, if one exists, and
-        course is not invitation only.
-        """
-
-        # if using registration method to restrict (say shibboleth)
-        if settings.FEATURES.get('RESTRICT_ENROLL_BY_REG_METHOD') and course.enrollment_domain:
-            if user is not None and user.is_authenticated() and \
-                    ExternalAuthMap.objects.filter(user=user, external_domain=course.enrollment_domain):
-                debug("Allow: external_auth of " + course.enrollment_domain)
-                reg_method_ok = True
-            else:
-                reg_method_ok = False
-        else:
-            reg_method_ok = True  # if not using this access check, it's always OK.
-
-        now = datetime.now(UTC())
-        start = course.enrollment_start or datetime.min.replace(tzinfo=pytz.UTC)
-        end = course.enrollment_end or datetime.max.replace(tzinfo=pytz.UTC)
-
-        # if user is in CourseEnrollmentAllowed with right course key then can also enroll
-        # (note that course.id actually points to a CourseKey)
-        # (the filter call uses course_id= since that's the legacy database schema)
-        # (sorry that it's confusing :( )
-        if user is not None and user.is_authenticated() and CourseEnrollmentAllowed:
-            if CourseEnrollmentAllowed.objects.filter(email=user.email, course_id=course.id):
-                return ACCESS_GRANTED
-
-        if _has_staff_access_to_descriptor(user, course, course.id):
-            return ACCESS_GRANTED
-
-        # Invitation_only doesn't apply to CourseEnrollmentAllowed or has_staff_access_access
-        if course.invitation_only:
-            debug("Deny: invitation only")
-            return ACCESS_DENIED
-
-        if reg_method_ok and start < now < end:
-            debug("Allow: in enrollment period")
-            return ACCESS_GRANTED
+        return _can_enroll_courselike(user, course)
 
     def see_exists():
         """
@@ -412,6 +417,7 @@ def _can_load_course_overview(user, course_overview):
     )
 
 _COURSE_OVERVIEW_CHECKERS = {
+    'enroll': _can_enroll_courselike,
     'load': _can_load_course_overview,
     'load_mobile': lambda user, course_overview: (
         _can_load_course_overview(user, course_overview)
diff --git a/lms/djangoapps/courseware/tests/test_access.py b/lms/djangoapps/courseware/tests/test_access.py
index 8d32023d5c7f663b04243de1346fdf34adf0ed80..109a77e32be5659f1bdf0f63bf62fa79d94c2e3b 100644
--- a/lms/djangoapps/courseware/tests/test_access.py
+++ b/lms/djangoapps/courseware/tests/test_access.py
@@ -515,6 +515,12 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
         self.user_staff = UserFactory.create(is_staff=True)
         self.user_anonymous = AnonymousUserFactory.create()
 
+    ENROLL_TEST_DATA = list(itertools.product(
+        ['user_normal', 'user_staff', 'user_anonymous'],
+        ['enroll'],
+        ['course_default', 'course_started', 'course_not_started', 'course_staff_only'],
+    ))
+
     LOAD_TEST_DATA = list(itertools.product(
         ['user_normal', 'user_beta_tester', 'user_staff'],
         ['load'],
@@ -533,7 +539,7 @@ class CourseOverviewAccessTestCase(ModuleStoreTestCase):
         ['course_default', 'course_with_pre_requisite', 'course_with_pre_requisites'],
     ))
 
-    @ddt.data(*(LOAD_TEST_DATA + LOAD_MOBILE_TEST_DATA + PREREQUISITES_TEST_DATA))
+    @ddt.data(*(ENROLL_TEST_DATA + LOAD_TEST_DATA + LOAD_MOBILE_TEST_DATA + PREREQUISITES_TEST_DATA))
     @ddt.unpack
     def test_course_overview_access(self, user_attr_name, action, course_attr_name):
         """
diff --git a/openedx/core/djangoapps/content/course_overviews/migrations/0004_auto__add_field_courseoverview_enrollment_start__add_field_courseoverv.py b/openedx/core/djangoapps/content/course_overviews/migrations/0004_auto__add_field_courseoverview_enrollment_start__add_field_courseoverv.py
new file mode 100644
index 0000000000000000000000000000000000000000..01a28b368949ef4e1caed6efc289ee556993b9b6
--- /dev/null
+++ b/openedx/core/djangoapps/content/course_overviews/migrations/0004_auto__add_field_courseoverview_enrollment_start__add_field_courseoverv.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # The default values for these new columns may not match the actual
+        # values of courses already present in the table. To ensure that the
+        # cached values are correct, we must clear the table before adding any
+        # new columns.
+        db.clear_table('course_overviews_courseoverview')
+
+        # Adding field 'CourseOverview.enrollment_start'
+        db.add_column('course_overviews_courseoverview', 'enrollment_start',
+                      self.gf('django.db.models.fields.DateTimeField')(null=True),
+                      keep_default=False)
+
+        # Adding field 'CourseOverview.enrollment_end'
+        db.add_column('course_overviews_courseoverview', 'enrollment_end',
+                      self.gf('django.db.models.fields.DateTimeField')(null=True),
+                      keep_default=False)
+
+        # Adding field 'CourseOverview.enrollment_domain'
+        db.add_column('course_overviews_courseoverview', 'enrollment_domain',
+                      self.gf('django.db.models.fields.TextField')(null=True),
+                      keep_default=False)
+
+        # Adding field 'CourseOverview.invitation_only'
+        db.add_column('course_overviews_courseoverview', 'invitation_only',
+                      self.gf('django.db.models.fields.BooleanField')(default=False),
+                      keep_default=False)
+
+        # Adding field 'CourseOverview.max_student_enrollments_allowed'
+        db.add_column('course_overviews_courseoverview', 'max_student_enrollments_allowed',
+                      self.gf('django.db.models.fields.IntegerField')(null=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'CourseOverview.enrollment_start'
+        db.delete_column('course_overviews_courseoverview', 'enrollment_start')
+
+        # Deleting field 'CourseOverview.enrollment_end'
+        db.delete_column('course_overviews_courseoverview', 'enrollment_end')
+
+        # Deleting field 'CourseOverview.enrollment_domain'
+        db.delete_column('course_overviews_courseoverview', 'enrollment_domain')
+
+        # Deleting field 'CourseOverview.invitation_only'
+        db.delete_column('course_overviews_courseoverview', 'invitation_only')
+
+        # Deleting field 'CourseOverview.max_student_enrollments_allowed'
+        db.delete_column('course_overviews_courseoverview', 'max_student_enrollments_allowed')
+
+
+    models = {
+        'course_overviews.courseoverview': {
+            'Meta': {'object_name': 'CourseOverview'},
+            '_location': ('xmodule_django.models.UsageKeyField', [], {'max_length': '255'}),
+            '_pre_requisite_courses_json': ('django.db.models.fields.TextField', [], {}),
+            'advertised_start': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'cert_html_view_enabled': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'cert_name_long': ('django.db.models.fields.TextField', [], {}),
+            'cert_name_short': ('django.db.models.fields.TextField', [], {}),
+            'certificates_display_behavior': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'certificates_show_before_end': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'course_image_url': ('django.db.models.fields.TextField', [], {}),
+            'days_early_for_beta': ('django.db.models.fields.FloatField', [], {'null': 'True'}),
+            'display_name': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'display_number_with_default': ('django.db.models.fields.TextField', [], {}),
+            'display_org_with_default': ('django.db.models.fields.TextField', [], {}),
+            'end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'end_of_course_survey_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'enrollment_domain': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'enrollment_end': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'enrollment_start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'facebook_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'has_any_active_web_certificate': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'id': ('xmodule_django.models.CourseKeyField', [], {'max_length': '255', 'primary_key': 'True', 'db_index': 'True'}),
+            'invitation_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'lowest_passing_grade': ('django.db.models.fields.DecimalField', [], {'max_digits': '5', 'decimal_places': '2'}),
+            'max_student_enrollments_allowed': ('django.db.models.fields.IntegerField', [], {'null': 'True'}),
+            'mobile_available': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'social_sharing_url': ('django.db.models.fields.TextField', [], {'null': 'True'}),
+            'start': ('django.db.models.fields.DateTimeField', [], {'null': 'True'}),
+            'visible_to_staff_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'})
+        }
+    }
+
+    complete_apps = ['course_overviews']
\ No newline at end of file
diff --git a/openedx/core/djangoapps/content/course_overviews/models.py b/openedx/core/djangoapps/content/course_overviews/models.py
index 4b8429f9cc7183d75a9ec4dffc7e4ba1f4aaca0e..517eda28a540d56587e50f54745d066491a4611c 100644
--- a/openedx/core/djangoapps/content/course_overviews/models.py
+++ b/openedx/core/djangoapps/content/course_overviews/models.py
@@ -5,7 +5,7 @@ Declaration of CourseOverview model
 import json
 
 import django.db.models
-from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField
+from django.db.models.fields import BooleanField, DateTimeField, DecimalField, TextField, FloatField, IntegerField
 from django.utils.translation import ugettext
 
 from util.date_utils import strftime_localized
@@ -60,6 +60,13 @@ class CourseOverview(django.db.models.Model):
     visible_to_staff_only = BooleanField()
     _pre_requisite_courses_json = TextField()  # JSON representation of list of CourseKey strings
 
+    # Enrollment details
+    enrollment_start = DateTimeField(null=True)
+    enrollment_end = DateTimeField(null=True)
+    enrollment_domain = TextField(null=True)
+    invitation_only = BooleanField(default=False)
+    max_student_enrollments_allowed = IntegerField(null=True)
+
     @staticmethod
     def _create_from_course(course):
         """
@@ -114,7 +121,13 @@ class CourseOverview(django.db.models.Model):
             days_early_for_beta=course.days_early_for_beta,
             mobile_available=course.mobile_available,
             visible_to_staff_only=course.visible_to_staff_only,
-            _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses)
+            _pre_requisite_courses_json=json.dumps(course.pre_requisite_courses),
+
+            enrollment_start=course.enrollment_start,
+            enrollment_end=course.enrollment_end,
+            enrollment_domain=course.enrollment_domain,
+            invitation_only=course.invitation_only,
+            max_student_enrollments_allowed=course.max_student_enrollments_allowed,
         )
 
     @staticmethod
diff --git a/openedx/core/djangoapps/content/course_overviews/signals.py b/openedx/core/djangoapps/content/course_overviews/signals.py
index 37bfcd268155b996847119b8a4f0356cc8956d4a..c2141847b12ff92f6067c24f82e3a97bda80f0f9 100644
--- a/openedx/core/djangoapps/content/course_overviews/signals.py
+++ b/openedx/core/djangoapps/content/course_overviews/signals.py
@@ -3,9 +3,8 @@ Signal handler for invalidating cached course overviews
 """
 from django.dispatch.dispatcher import receiver
 
-from xmodule.modulestore.django import SignalHandler
-
 from .models import CourseOverview
+from xmodule.modulestore.django import SignalHandler
 
 
 @receiver(SignalHandler.course_published)
@@ -15,3 +14,12 @@ def _listen_for_course_publish(sender, course_key, **kwargs):  # pylint: disable
     invalidates the corresponding CourseOverview cache entry if one exists.
     """
     CourseOverview.objects.filter(id=course_key).delete()
+
+
+@receiver(SignalHandler.course_deleted)
+def _listen_for_course_delete(sender, course_key, **kwargs):  # pylint: disable=unused-argument
+    """
+    Catches the signal that a course has been deleted from Studio and
+    invalidates the corresponding CourseOverview cache entry if one exists.
+    """
+    CourseOverview.objects.filter(id=course_key).delete()
diff --git a/openedx/core/djangoapps/content/course_overviews/tests.py b/openedx/core/djangoapps/content/course_overviews/tests.py
index 8a56bb51725948be2e6e422215b1d96f6d74cde4..25035b26061d3f96a9fdb96a1d463746b8d0dfb8 100644
--- a/openedx/core/djangoapps/content/course_overviews/tests.py
+++ b/openedx/core/djangoapps/content/course_overviews/tests.py
@@ -91,6 +91,9 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
             'display_name_with_default',
             'start_date_is_still_default',
             'pre_requisite_courses',
+            'enrollment_domain',
+            'invitation_only',
+            'max_student_enrollments_allowed',
         ]
         for attribute_name in fields_to_test:
             course_value = getattr(course, attribute_name)
@@ -125,24 +128,38 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
         # resulting values are often off by fractions of a second. So, as a
         # workaround, we simply test if the start and end times are the same
         # number of seconds from the Unix epoch.
-        others_to_test = [(
-            course_image_url(course),
-            course_overview_cache_miss.course_image_url,
-            course_overview_cache_hit.course_image_url
-        ), (
-            get_active_web_certificate(course) is not None,
-            course_overview_cache_miss.has_any_active_web_certificate,
-            course_overview_cache_hit.has_any_active_web_certificate
-
-        ), (
-            get_seconds_since_epoch(course.start),
-            get_seconds_since_epoch(course_overview_cache_miss.start),
-            get_seconds_since_epoch(course_overview_cache_hit.start),
-        ), (
-            get_seconds_since_epoch(course.end),
-            get_seconds_since_epoch(course_overview_cache_miss.end),
-            get_seconds_since_epoch(course_overview_cache_hit.end),
-        )]
+        others_to_test = [
+            (
+                course_image_url(course),
+                course_overview_cache_miss.course_image_url,
+                course_overview_cache_hit.course_image_url
+            ),
+            (
+                get_active_web_certificate(course) is not None,
+                course_overview_cache_miss.has_any_active_web_certificate,
+                course_overview_cache_hit.has_any_active_web_certificate
+            ),
+            (
+                get_seconds_since_epoch(course.start),
+                get_seconds_since_epoch(course_overview_cache_miss.start),
+                get_seconds_since_epoch(course_overview_cache_hit.start),
+            ),
+            (
+                get_seconds_since_epoch(course.end),
+                get_seconds_since_epoch(course_overview_cache_miss.end),
+                get_seconds_since_epoch(course_overview_cache_hit.end),
+            ),
+            (
+                get_seconds_since_epoch(course.enrollment_start),
+                get_seconds_since_epoch(course_overview_cache_miss.enrollment_start),
+                get_seconds_since_epoch(course_overview_cache_hit.enrollment_start),
+            ),
+            (
+                get_seconds_since_epoch(course.enrollment_end),
+                get_seconds_since_epoch(course_overview_cache_miss.enrollment_end),
+                get_seconds_since_epoch(course_overview_cache_hit.enrollment_end),
+            ),
+        ]
         for (course_value, cache_miss_value, cache_hit_value) in others_to_test:
             self.assertEqual(course_value, cache_miss_value)
             self.assertEqual(cache_miss_value, cache_hit_value)
@@ -211,7 +228,7 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
     @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
     def test_course_overview_cache_invalidation(self, modulestore_type):
         """
-        Tests that when a course is published, the corresponding
+        Tests that when a course is published or deleted, the corresponding
         course_overview is removed from the cache.
 
         Arguments:
@@ -236,6 +253,11 @@ class CourseOverviewTestCase(ModuleStoreTestCase):
             course_overview_2 = CourseOverview.get_from_id(course.id)
             self.assertFalse(course_overview_2.mobile_available)
 
+            # Verify that when the course is deleted, the corresponding CourseOverview is deleted as well.
+            with self.assertRaises(CourseOverview.DoesNotExist):
+                self.store.delete_course(course.id, ModuleStoreEnum.UserID.test)
+                CourseOverview.get_from_id(course.id)
+
     @ddt.data((ModuleStoreEnum.Type.mongo, 1, 1), (ModuleStoreEnum.Type.split, 3, 4))
     @ddt.unpack
     def test_course_overview_caching(self, modulestore_type, min_mongo_calls, max_mongo_calls):