Skip to content

Service-owned UI modules

In a microservices architecture, each service owns and exposes its own UI. A shell application aggregates them into a unified interface using RemoteMenu. This is the pattern that makes Mateu a UI orchestration layer — not a monolithic frontend that all teams must coordinate.

Prerequisite: understand Mateu in hexagonal architecture — service-owned UI is the natural consequence of treating the UI as an inbound adapter per bounded context.


Each service defines its own @UI and navigation:

// In the content-service backend
@UI("/_content-service")
public class ContentServiceApp {
@Menu
Pages pages;
@Menu
Templates templates;
@Menu
@EyesOnly(roles = "admin")
Settings settings;
}

The shell application pulls the remote service’s menu and embeds it:

// In the shell / backoffice
@UI("")
public class Shell {
@Menu
Home home;
@Menu
RemoteMenu contentService = new RemoteMenu(
"Content",
"https://content-service.internal/_content-service"
);
@Menu
RemoteMenu analyticsService = new RemoteMenu(
"Analytics",
"https://analytics-service.internal/_analytics"
);
}

The shell renders the combined navigation. Clicking a remote menu item proxies the request to the owning service.

flowchart TD
Browser --> Shell["Shell / Backoffice\n@UI(\"\")"]
Shell -->|RemoteMenu| CS["Content Service\n@UI(\"/_content-service\")"]
Shell -->|RemoteMenu| AS["Analytics Service\n@UI(\"/_analytics\")"]
Shell -->|local menu| Home["Home page"]
CS --> CSMenu["Pages · Templates · Settings"]
AS --> ASMenu["Dashboards · Reports"]

ConcernBenefit
Independent deploymentEach service’s UI deploys with the service
Clear boundariesUI logic lives with domain logic
No shared frontendNo coordination of a monolithic frontend repo
AuthorizationEach service enforces its own @EyesOnly rules
TestabilityEach UI module can be tested in isolation

Each service is a standard Mateu application:

@UI("/_orders")
public class OrdersServiceUI {
@Menu
OrdersOrchestrator orders;
@Menu
@EyesOnly(roles = "manager")
ReportsPage reports;
}

The @UI path acts as the namespace for all routes in that service.


Pages within a service use standard @Route with parentRoute:

@Route(value = "/_orders/order-detail/:id", parentRoute = "/_orders")
public class OrderDetailPage implements ComponentTreeSupplier {
// ...
}

Routes are scoped to the service. The shell navigates to them by assembling service-base-url + route.


Each service validates @EyesOnly independently using the JWT token forwarded by the shell:

User browser → Shell (aggregates menus) → Service backend (validates @EyesOnly)

A user who lacks the required role sees the menu entry hidden in the service’s response — not just in the shell.



A simpler variant of the same idea: package your @UI classes into a plain Java library and let one or more Spring Boot apps depend on it. All services compile into a single deployable; no RemoteMenu or HTTP federation required.

The library only needs io.mateu:uidl — no framework dependency:

<dependency>
<groupId>io.mateu</groupId>
<artifactId>uidl</artifactId>
<version>${mateu.version}</version>
</dependency>

Add the indexer annotation processor so that the library’s @UI classes are discoverable by downstream modules at compile time:

<dependency>
<groupId>io.mateu</groupId>
<artifactId>annotation-processor-indexer</artifactId>
<version>${mateu.version}</version>
<scope>provided</scope>
</dependency>

And configure it in the compiler plugin:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.mateu</groupId>
<artifactId>annotation-processor-indexer</artifactId>
<version>${mateu.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

When the library is compiled, MateuUIIndexerProcessor writes a manifest META-INF/mateu/ui-registrations into the jar listing every @UI class.

Add the library to <dependencies> as usual, and also to <annotationProcessorPaths> so the framework-specific processor can read the manifest at compile time:

<!-- regular runtime dependency -->
<dependency>
<groupId>com.example</groupId>
<artifactId>my-ui-lib</artifactId>
<version>1.0.0</version>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path><!-- lombok --></path>
<path>
<groupId>io.mateu</groupId>
<artifactId>annotation-processor-mvc</artifactId>
<version>${mateu.version}</version>
</path>
<!-- also put the UI library here so the processor can read
META-INF/mateu/ui-registrations from its jar -->
<path>
<groupId>com.example</groupId>
<artifactId>my-ui-lib</artifactId>
<version>1.0.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

Mateu’s MateuIndexedUIProcessor fires during the app’s compilation, reads the manifest from the library jar, and generates the controllers (MyPageController, MyPageMateuController, …) exactly as if the @UI classes were in the app’s own sources.

Why does the library need to be on the annotation processor classpath? Annotation processors run in their own classloader, separate from the regular compile classpath. Adding the library to <annotationProcessorPaths> ensures the processor can load META-INF/mateu/ui-registrations from the jar.