Recent Posts
Archives

Posts Tagged ‘BoundedContext’

PostHeaderIcon [VoxxedDaysAmsterdam2026] Taming Microservice Overload: The Journey to Modular Monoliths at the Port of Rotterdam

Lecturer
Mario Klaver is a solution architect at the Port of Rotterdam, responsible for digital systems supporting Harbour Master operations. With a background as a developer, he remains actively involved in programming and puzzle-solving.

Abstract
Mario Klaver recounts the evolution of the Port of Rotterdam’s mission-critical harbor management system from a monolithic application through an explosion of microservices to a more sustainable modular monolith architecture. Using Domain-Driven Design (DDD) techniques such as EventStorming and bounded contexts, combined with Spring Modulith, the team consolidated over 90 microservices into fewer, well-bounded services aligned with business domains. The presentation details the motivations, challenges, implementation approach, migration strategy, and lessons learned, offering practical insights for organizations facing similar architectural fatigue.

From Monolith to Microservices: The Growth of Complexity

In 2010, the Port of Rotterdam initiated a new 24/7 mission-critical system for the Harbour Master division using then-contemporary technologies like Java and Swing (with offline requirements precluding full HTML5 adoption). By 2015, a single backend had become unmaintainable spaghetti code with lengthy manual regression tests.

Following conventional wisdom, the team adopted microservices in 2016, starting with 10 services. By 2018 this grew to 50, and eventually exceeded 90. Every new feature or subject—vessel visits, inspections, dangerous goods, waste handling—spawned additional services, sometimes multiple per domain. Rather than reducing complexity, microservices distributed it, creating tight coupling, high cloud costs (exacerbated by multiple environments and ports), extensive update overhead, and paradoxically slower delivery despite supposed team autonomy.

Anonymized architecture diagrams revealed that services appeared logically separated by responsibility (vessel visits, conflict signaling, clearance) yet events and synchronous calls created a web of dependencies where boundaries were unclear or misplaced. The realization emerged: microservices had not fixed the architecture; they had merely distributed the mess.

Rediscovering Domain Boundaries with EventStorming and DDD

The turning point involved shifting focus from technical partitioning to domain alignment. EventStorming workshops proved instrumental—a collaborative, chaotic yet powerful technique using brown paper, stickies, and domain events to map complex business processes. Participants from business, development, architecture, UX, and operations contributed partial knowledge to build a shared understanding.

Pivotal events, parallel flows, and heuristics helped surface natural subdomains (e.g., Visit, Clearance) and bounded contexts within them. Digitized results clarified responsibilities that differed markedly from existing service implementations. Good boundaries were defined by low coupling (changes mostly contained within a module or service) and high cohesion (modules focused on a clear purpose).

The decision was made to move toward one (larger) service per subdomain, implemented as modular monoliths to prevent internal spaghetti while preserving clear interfaces.

Implementing Modular Monoliths with Spring Modulith

Spring Modulith aligns perfectly with DDD bounded contexts. After adding the dependency, packages directly under the application entry point become modules by convention. Public APIs (Spring beans or events) are exposed in designated packages or via @NamedInterface annotations, while other packages remain private and inaccessible across modules.

IntelliJ provides excellent visual support with green/red indicators for public/private elements. The @ApplicationModule annotation enforces allowed dependencies and prevents cycles. An open/closed mode facilitates gradual migration. Crucially, architecture tests (using @ApplicationModuleTest) verify module boundaries, API usage, and absence of cycles—failing builds if violated.

Inter-module communication prefers Spring’s event publishing and consumption mechanisms. Synchronous listeners use @TransactionalEventListener, while asynchronous ones leverage Modulith’s @ApplicationModuleListener, which persists events for reliability across restarts and decouples transactions. This design eases future extraction of modules into separate services if needed.

Module-specific tests spin up isolated Spring contexts, enabling verification of event flows (e.g., publishing a security clearance event and asserting a downstream vessel movement decision).

Migration Strategy and Architectural Trade-offs

A full green-field rebuild was deemed impractical given a roughly five-year horizon. Instead, the team adopted an incremental “heart surgery while the patient lives” approach: create walking skeletons for new modular services, migrate relevant legacy services as modules, reroute connections, and decommission old instances using feature flags to avoid duplicate processing. Databases initially remain shared before eventual per-module schema separation and a broader Oracle-to-PostgreSQL migration.

Takeaways include faster development and simpler debugging within a single service, reduced infrastructure, but larger deployables requiring broader regression testing, reduced independent scalability, and greater impact during outages.

Lessons Learned and Broader Implications

Key lessons encompass inclusive EventStorming participation to avoid blind spots, ongoing domain exploration and validation with real scenarios, and persistence with clean architectures (e.g., onion/hexagonal) despite mapping overhead—which AI may eventually alleviate. Boundaries matter more than the number of services. A modular monolith often serves as the ideal starting point or interim step before microservices, as correcting boundaries post-split is far costlier.

Mario concludes that microservices distribute complexity, whereas well-designed boundaries control it. The Port of Rotterdam’s journey illustrates a pragmatic path for organizations overwhelmed by service proliferation.

Code Sample: Spring Modulith Module Configuration

@ApplicationModule(
    allowedDependencies = {"securityClearance", "vesselVisit"},
    type = ApplicationModule.Type.OPEN  // during migration
)
public class PortClearanceApplication { ... }

Code Sample: Event Consumption

@TransactionalEventListener
public void onSecurityClearanceCompleted(SecurityClearanceCompletedEvent event) {
    // process within transaction
}

@ApplicationModuleListener
public void onAsyncEvent(AnotherEvent event) {
    // decoupled, persisted for reliability
}

These patterns enforce architectural integrity while leveraging familiar Spring mechanisms.

In reflecting on the Port of Rotterdam’s experience, Mario Klaver delivers a compelling case study in architectural modernization. By realigning services with domain realities through DDD and enforcing modularity with Spring Modulith, the organization has regained clarity, ownership, and sustainable evolution capacity without sacrificing the benefits of service-oriented thinking.

Links: