diff --git a/src/backend/Domain/Domain.csproj b/src/backend/Domain/Domain.csproj
new file mode 100644
index 0000000..93f5ab4
--- /dev/null
+++ b/src/backend/Domain/Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/src/backend/Domain/Entities/Enrollment.cs b/src/backend/Domain/Entities/Enrollment.cs
new file mode 100644
index 0000000..43e2306
--- /dev/null
+++ b/src/backend/Domain/Entities/Enrollment.cs
@@ -0,0 +1,20 @@
+namespace Domain.Entities;
+
+public class Enrollment
+{
+ public int Id { get; private set; }
+ public int StudentId { get; private set; }
+ public Student Student { get; private set; } = null!;
+ public int SubjectId { get; private set; }
+ public Subject Subject { get; private set; } = null!;
+ public DateTime EnrolledAt { get; private set; }
+
+ private Enrollment() { }
+
+ public Enrollment(int studentId, int subjectId)
+ {
+ StudentId = studentId;
+ SubjectId = subjectId;
+ EnrolledAt = DateTime.UtcNow;
+ }
+}
diff --git a/src/backend/Domain/Entities/Professor.cs b/src/backend/Domain/Entities/Professor.cs
new file mode 100644
index 0000000..c00fe28
--- /dev/null
+++ b/src/backend/Domain/Entities/Professor.cs
@@ -0,0 +1,28 @@
+namespace Domain.Entities;
+
+public class Professor
+{
+ public int Id { get; private set; }
+ public string Name { get; private set; } = string.Empty;
+
+ private readonly List _subjects = [];
+ public IReadOnlyCollection Subjects => _subjects.AsReadOnly();
+
+ private Professor() { }
+
+ public Professor(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Professor name is required", nameof(name));
+
+ Name = name;
+ }
+
+ public void AddSubject(Subject subject)
+ {
+ if (_subjects.Count >= 2)
+ throw new InvalidOperationException("A professor can only teach 2 subjects");
+
+ _subjects.Add(subject);
+ }
+}
diff --git a/src/backend/Domain/Entities/Student.cs b/src/backend/Domain/Entities/Student.cs
new file mode 100644
index 0000000..d7b63a9
--- /dev/null
+++ b/src/backend/Domain/Entities/Student.cs
@@ -0,0 +1,56 @@
+namespace Domain.Entities;
+
+using Domain.ValueObjects;
+
+public class Student
+{
+ public const int MaxEnrollments = 3;
+
+ public int Id { get; private set; }
+ public string Name { get; private set; } = string.Empty;
+ public Email Email { get; private set; } = null!;
+
+ private readonly List _enrollments = [];
+ public IReadOnlyCollection Enrollments => _enrollments.AsReadOnly();
+
+ public int TotalCredits => _enrollments.Sum(e => e.Subject?.Credits ?? Subject.CreditsPerSubject);
+
+ private Student() { }
+
+ public Student(string name, Email email)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Student name is required", nameof(name));
+
+ Name = name;
+ Email = email ?? throw new ArgumentNullException(nameof(email));
+ }
+
+ public void UpdateName(string name)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Student name is required", nameof(name));
+
+ Name = name;
+ }
+
+ public void UpdateEmail(Email email)
+ {
+ Email = email ?? throw new ArgumentNullException(nameof(email));
+ }
+
+ public bool HasProfessor(int professorId) =>
+ _enrollments.Any(e => e.Subject?.ProfessorId == professorId);
+
+ public bool CanEnroll() => _enrollments.Count < MaxEnrollments;
+
+ public void AddEnrollment(Enrollment enrollment)
+ {
+ _enrollments.Add(enrollment);
+ }
+
+ public void RemoveEnrollment(Enrollment enrollment)
+ {
+ _enrollments.Remove(enrollment);
+ }
+}
diff --git a/src/backend/Domain/Entities/Subject.cs b/src/backend/Domain/Entities/Subject.cs
new file mode 100644
index 0000000..02c82e2
--- /dev/null
+++ b/src/backend/Domain/Entities/Subject.cs
@@ -0,0 +1,27 @@
+namespace Domain.Entities;
+
+public class Subject
+{
+ public const int CreditsPerSubject = 3;
+
+ public int Id { get; private set; }
+ public string Name { get; private set; } = string.Empty;
+ public int Credits { get; private set; } = CreditsPerSubject;
+ public int ProfessorId { get; private set; }
+ public Professor Professor { get; private set; } = null!;
+
+ private readonly List _enrollments = [];
+ public IReadOnlyCollection Enrollments => _enrollments.AsReadOnly();
+
+ private Subject() { }
+
+ public Subject(string name, int professorId)
+ {
+ if (string.IsNullOrWhiteSpace(name))
+ throw new ArgumentException("Subject name is required", nameof(name));
+
+ Name = name;
+ Credits = CreditsPerSubject;
+ ProfessorId = professorId;
+ }
+}
diff --git a/src/backend/Domain/Exceptions/DomainException.cs b/src/backend/Domain/Exceptions/DomainException.cs
new file mode 100644
index 0000000..dfcd94e
--- /dev/null
+++ b/src/backend/Domain/Exceptions/DomainException.cs
@@ -0,0 +1,47 @@
+namespace Domain.Exceptions;
+
+public abstract class DomainException : Exception
+{
+ public string Code { get; }
+
+ protected DomainException(string code, string message) : base(message)
+ {
+ Code = code;
+ }
+}
+
+public class MaxEnrollmentsExceededException : DomainException
+{
+ public MaxEnrollmentsExceededException(int studentId)
+ : base("MAX_ENROLLMENTS", $"Student {studentId} cannot enroll in more than 3 subjects") { }
+}
+
+public class SameProfessorConstraintException : DomainException
+{
+ public SameProfessorConstraintException(int studentId, int professorId)
+ : base("SAME_PROFESSOR", $"Student {studentId} already has a subject with professor {professorId}") { }
+}
+
+public class StudentNotFoundException : DomainException
+{
+ public StudentNotFoundException(int studentId)
+ : base("STUDENT_NOT_FOUND", $"Student {studentId} was not found") { }
+}
+
+public class SubjectNotFoundException : DomainException
+{
+ public SubjectNotFoundException(int subjectId)
+ : base("SUBJECT_NOT_FOUND", $"Subject {subjectId} was not found") { }
+}
+
+public class EnrollmentNotFoundException : DomainException
+{
+ public EnrollmentNotFoundException(int enrollmentId)
+ : base("ENROLLMENT_NOT_FOUND", $"Enrollment {enrollmentId} was not found") { }
+}
+
+public class DuplicateEnrollmentException : DomainException
+{
+ public DuplicateEnrollmentException(int studentId, int subjectId)
+ : base("DUPLICATE_ENROLLMENT", $"Student {studentId} is already enrolled in subject {subjectId}") { }
+}
diff --git a/src/backend/Domain/Ports/Repositories/IEnrollmentRepository.cs b/src/backend/Domain/Ports/Repositories/IEnrollmentRepository.cs
new file mode 100644
index 0000000..71af85f
--- /dev/null
+++ b/src/backend/Domain/Ports/Repositories/IEnrollmentRepository.cs
@@ -0,0 +1,16 @@
+namespace Domain.Ports.Repositories;
+
+using Domain.Entities;
+
+public interface IEnrollmentRepository
+{
+ Task GetByIdAsync(int id, CancellationToken ct = default);
+ Task GetByStudentAndSubjectAsync(int studentId, int subjectId, CancellationToken ct = default);
+ Task> GetByStudentIdAsync(int studentId, CancellationToken ct = default);
+ Task> GetBySubjectIdAsync(int subjectId, CancellationToken ct = default);
+ Task> GetClassmatesAsync(int studentId, int subjectId, CancellationToken ct = default);
+ Task>> GetClassmatesBatchAsync(
+ int studentId, IEnumerable subjectIds, CancellationToken ct = default);
+ void Add(Enrollment enrollment);
+ void Delete(Enrollment enrollment);
+}
diff --git a/src/backend/Domain/Ports/Repositories/IProfessorRepository.cs b/src/backend/Domain/Ports/Repositories/IProfessorRepository.cs
new file mode 100644
index 0000000..4aac2e6
--- /dev/null
+++ b/src/backend/Domain/Ports/Repositories/IProfessorRepository.cs
@@ -0,0 +1,16 @@
+namespace Domain.Ports.Repositories;
+
+using Domain.Entities;
+using System.Linq.Expressions;
+
+public interface IProfessorRepository
+{
+ Task GetByIdAsync(int id, CancellationToken ct = default);
+ Task> GetAllAsync(CancellationToken ct = default);
+ Task> GetAllWithSubjectsAsync(CancellationToken ct = default);
+
+ // Projection support
+ Task> GetAllProjectedAsync(
+ Expression> selector,
+ CancellationToken ct = default);
+}
diff --git a/src/backend/Domain/Ports/Repositories/IStudentRepository.cs b/src/backend/Domain/Ports/Repositories/IStudentRepository.cs
new file mode 100644
index 0000000..e58c4ab
--- /dev/null
+++ b/src/backend/Domain/Ports/Repositories/IStudentRepository.cs
@@ -0,0 +1,34 @@
+namespace Domain.Ports.Repositories;
+
+using Domain.Entities;
+using System.Linq.Expressions;
+
+public interface IStudentRepository
+{
+ Task GetByIdAsync(int id, CancellationToken ct = default);
+ Task GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default);
+ Task> GetAllAsync(CancellationToken ct = default);
+ Task> GetAllWithEnrollmentsAsync(CancellationToken ct = default);
+ Task ExistsAsync(int id, CancellationToken ct = default);
+ Task EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default);
+
+ // Projection support for optimized queries
+ Task> GetAllProjectedAsync(
+ Expression> selector,
+ CancellationToken ct = default);
+ Task GetByIdProjectedAsync(
+ int id,
+ Expression> selector,
+ CancellationToken ct = default);
+
+ // Keyset pagination (more efficient than OFFSET)
+ Task<(IReadOnlyList Items, int? NextCursor, int TotalCount)> GetPagedProjectedAsync(
+ Expression> selector,
+ int? afterId = null,
+ int pageSize = 10,
+ CancellationToken ct = default);
+
+ void Add(Student student);
+ void Update(Student student);
+ void Delete(Student student);
+}
diff --git a/src/backend/Domain/Ports/Repositories/ISubjectRepository.cs b/src/backend/Domain/Ports/Repositories/ISubjectRepository.cs
new file mode 100644
index 0000000..b62ac65
--- /dev/null
+++ b/src/backend/Domain/Ports/Repositories/ISubjectRepository.cs
@@ -0,0 +1,18 @@
+namespace Domain.Ports.Repositories;
+
+using Domain.Entities;
+using System.Linq.Expressions;
+
+public interface ISubjectRepository
+{
+ Task GetByIdAsync(int id, CancellationToken ct = default);
+ Task GetByIdWithProfessorAsync(int id, CancellationToken ct = default);
+ Task> GetAllAsync(CancellationToken ct = default);
+ Task> GetAllWithProfessorsAsync(CancellationToken ct = default);
+ Task> GetAvailableForStudentAsync(int studentId, CancellationToken ct = default);
+
+ // Projection support
+ Task> GetAllProjectedAsync(
+ Expression> selector,
+ CancellationToken ct = default);
+}
diff --git a/src/backend/Domain/Ports/Repositories/IUnitOfWork.cs b/src/backend/Domain/Ports/Repositories/IUnitOfWork.cs
new file mode 100644
index 0000000..5c752b1
--- /dev/null
+++ b/src/backend/Domain/Ports/Repositories/IUnitOfWork.cs
@@ -0,0 +1,6 @@
+namespace Domain.Ports.Repositories;
+
+public interface IUnitOfWork
+{
+ Task SaveChangesAsync(CancellationToken ct = default);
+}
diff --git a/src/backend/Domain/Services/EnrollmentDomainService.cs b/src/backend/Domain/Services/EnrollmentDomainService.cs
new file mode 100644
index 0000000..22cc7f2
--- /dev/null
+++ b/src/backend/Domain/Services/EnrollmentDomainService.cs
@@ -0,0 +1,29 @@
+namespace Domain.Services;
+
+using Domain.Entities;
+using Domain.Exceptions;
+
+public class EnrollmentDomainService
+{
+ public void ValidateEnrollment(Student student, Subject subject)
+ {
+ if (!student.CanEnroll())
+ throw new MaxEnrollmentsExceededException(student.Id);
+
+ if (student.HasProfessor(subject.ProfessorId))
+ throw new SameProfessorConstraintException(student.Id, subject.ProfessorId);
+
+ if (student.Enrollments.Any(e => e.SubjectId == subject.Id))
+ throw new DuplicateEnrollmentException(student.Id, subject.Id);
+ }
+
+ public Enrollment CreateEnrollment(Student student, Subject subject)
+ {
+ ValidateEnrollment(student, subject);
+
+ var enrollment = new Enrollment(student.Id, subject.Id);
+ student.AddEnrollment(enrollment);
+
+ return enrollment;
+ }
+}
diff --git a/src/backend/Domain/ValueObjects/Email.cs b/src/backend/Domain/ValueObjects/Email.cs
new file mode 100644
index 0000000..74c565f
--- /dev/null
+++ b/src/backend/Domain/ValueObjects/Email.cs
@@ -0,0 +1,32 @@
+namespace Domain.ValueObjects;
+
+using System.Text.RegularExpressions;
+
+public partial class Email
+{
+ public string Value { get; }
+
+ private Email(string value) => Value = value;
+
+ public static Email Create(string email)
+ {
+ if (string.IsNullOrWhiteSpace(email))
+ throw new ArgumentException("Email is required", nameof(email));
+
+ email = email.Trim().ToLowerInvariant();
+
+ if (!EmailRegex().IsMatch(email))
+ throw new ArgumentException("Invalid email format", nameof(email));
+
+ return new Email(email);
+ }
+
+ public override string ToString() => Value;
+ public override bool Equals(object? obj) => obj is Email other && Value == other.Value;
+ public override int GetHashCode() => Value.GetHashCode();
+
+ public static implicit operator string(Email email) => email.Value;
+
+ [GeneratedRegex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled)]
+ private static partial Regex EmailRegex();
+}