Enhancing Maintainability: An 'Amelioration' Journey in the rdv_Medecin Application

The Problem

In the rdv_Medecin project, which facilitates medical appointments, we identified a growing challenge: as features expanded, the core domain logic was becoming increasingly intertwined with data persistence concerns and UI-specific orchestrations. This blurring of lines led to code that was harder to test, more prone to regressions, and slower to develop new features. Imagine a tangled ball of yarn where pulling one thread risks unraveling the entire garment – that's what our domain logic started to feel like.

The primary symptom was a lack of clear boundaries within our Java application. Entities sometimes directly managed database operations, and service layers became bloated with decision-making that rightfully belonged to the domain itself. This made it difficult to reason about the system's behavior and slowed down our development velocity significantly.

The Approach

To address these issues and streamline our codebase – a process we internally referred to as 'amelioration' – we embarked on a focused effort to reinforce Domain-Driven Design (DDD) principles and clarify responsibilities. Our approach unfolded in three key phases:

Phase 1: Solidifying Domain Entities and Value Objects

The first step was to ensure our core domain entities and value objects were truly robust, encapsulating business rules and protecting their invariants. This meant moving business logic from services directly into the entities themselves, making them 'behavior-rich'. For instance, a RendezVous (Appointment) entity should know how to validate its own schedule constraints, rather than a service performing that check.

public class RendezVous {
    private UUID id;
    private MedecinId medecinId;
    private PatientId patientId;
    private LocalDateTime debut;
    private LocalDateTime fin;

    public RendezVous(MedecinId medecinId, PatientId patientId, LocalDateTime debut, LocalDateTime fin) {
        if (debut.isAfter(fin)) {
            throw new IllegalArgumentException("Start time must be before end time.");
        }
        if (debut.isBefore(LocalDateTime.now())) {
            throw new IllegalArgumentException("Cannot schedule an appointment in the past.");
        }
        // More complex business rules for availability, overlap, etc., would go here
        this.id = UUID.randomUUID();
        this.medecinId = medecinId;
        this.patientId = patientId;
        this.debut = debut;
        this.fin = fin;
    }

    // Method to reschedule, encapsulating rescheduling logic
    public void reschedule(LocalDateTime newDebut, LocalDateTime newFin) {
        if (newDebut.isAfter(newFin)) {
            throw new IllegalArgumentException("New start time must be before new end time.");
        }
        if (newDebut.isBefore(LocalDateTime.now())) {
            throw new IllegalArgumentException("Cannot reschedule to the past.");
        }
        this.debut = newDebut;
        this.fin = newFin;
        // Potentially publish a Domain Event: RendezVousRescheduledEvent
    }

    // Getters...
}

Phase 2: Refined Repository Interactions with PostgreSQL

With stronger domain models, we focused on refining how they interact with our PostgreSQL database. The goal was to ensure repositories were true collections of domain objects, abstracting away persistence details. We ensured transactional boundaries were clearly defined at the service layer, preventing partial updates and maintaining data integrity.

@Repository
public class RendezVousPostgresRepository implements RendezVousRepository {

    private final JdbcTemplate jdbcTemplate;

    public RendezVousPostgresRepository(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Override
    public RendezVous findById(UUID id) {
        String sql = "SELECT id, medecin_id, patient_id, debut, fin FROM rdv_appointments WHERE id = ?";
        return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> {
            // Map ResultSet to RendezVous entity
            return new RendezVous(
                    new MedecinId(rs.getString("medecin_id")),
                    new PatientId(rs.getString("patient_id")),
                    rs.getTimestamp("debut").toLocalDateTime(),
                    rs.getTimestamp("fin").toLocalDateTime()
            );
        });
    }

    @Override
    public void save(RendezVous rendezVous) {
        String sql = "INSERT INTO rdv_appointments (id, medecin_id, patient_id, debut, fin) VALUES (?, ?, ?, ?, ?) " +
                     "ON CONFLICT (id) DO UPDATE SET medecin_id = EXCLUDED.medecin_id, patient_id = EXCLUDED.patient_id, debut = EXCLUDED.debut, fin = EXCLUDED.fin";
        jdbcTemplate.update(sql,
                rendezVous.getId(),
                rendezVous.getMedecinId().getValue(),
                rendezVous.getPatientId().getValue(),
                rendezVous.getDebut(),
                rendezVous.getFin()
        );
    }
    // Other repository methods...
}

Phase 3: Leaner Application Services

Finally, we revisited our application service layer. By pushing domain logic into entities and abstracting persistence behind repositories, application services became much leaner. Their primary role is now to orchestrate the domain objects, handle transaction management, and coordinate with infrastructure concerns (like sending emails or external API calls), without dictating business rules.

@Service
public class RendezVousSchedulingService {

    private final RendezVousRepository rendezVousRepository;
    private final MedecinAvailabilityService medecinAvailabilityService;

    public RendezVousSchedulingService(RendezVousRepository rendezVousRepository, MedecinAvailabilityService medecinAvailabilityService) {
        this.rendezVousRepository = rendezVousRepository;
        this.medecinAvailabilityService = medecinAvailabilityService;
    }

    @Transactional
    public RendezVous scheduleNewRendezVous(ScheduleAppointmentCommand command) {
        // Check business rules not encapsulated by RendezVous itself (e.g., global availability)
        if (!medecinAvailabilityService.isAvailable(command.getMedecinId(), command.getDebut(), command.getFin())) {
            throw new IllegalStateException("Doctor is not available at the requested time.");
        }
        
        RendezVous rendezVous = new RendezVous(
                command.getMedecinId(),
                command.getPatientId(),
                command.getDebut(),
                command.getFin()
        );
        rendezVousRepository.save(rendezVous);
        // Publish a domain event for other parts of the system to react
        return rendezVous;
    }

    @Transactional
    public void cancelRendezVous(UUID rendezVousId) {
        RendezVous rendezVous = rendezVousRepository.findById(rendezVousId);
        // RendezVous entity would contain logic for 'cancellation' if complex
        rendezVousRepository.delete(rendezVousId);
    }
}

Final Numbers

While this 'amelioration' wasn't about raw performance metrics, the qualitative improvements were significant:

Metric Before After
Code Readability Moderate High
Testability Challenging Significantly Improved
Feature Development Slower Faster
Bug Surface Area Higher Reduced
Maintainability Complex Streamlined

Key Insight

The most impactful change was consistently applying the principle of encapsulation within domain entities. By making entities responsible for their own state transitions and business rules, we unlocked clearer separation of concerns across our entire application. This not only improved the current codebase but also set a stronger foundation for future development of the rdv_Medecin application, making it more resilient to change and easier to understand.


Generated with Gitvlg.com

Enhancing Maintainability: An 'Amelioration' Journey in the rdv_Medecin Application
MendrikaNomentsoa

MendrikaNomentsoa

Author

Share: