Skip to content

refactor: enforce clean layer separation across repository, service, and controller #216

Open
anderslindho wants to merge 4 commits intomasterfrom
refactor/layer-isolation
Open

refactor: enforce clean layer separation across repository, service, and controller #216
anderslindho wants to merge 4 commits intomasterfrom
refactor/layer-isolation

Conversation

@anderslindho
Copy link
Copy Markdown
Contributor

Pushes the codebase toward cleaner layering: repositories are pure persistence adapters, services have no web framework dependencies, and the HTTP boundary is explicit.

Also adds a v0 DTO layer ahead of a planned v1 API, so the two can evolve independently without touching the service or domain layers.

* @return matching channels
*/
public SearchResult search(MultiValueMap<String, String> searchParameters) {
public SearchResult search(Map<String, List<String>> searchParameters) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why replacing MultiValuedMap?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See commit message for that change:

Services must not depend on Spring web binding types. Replace
MultiValueMap<String, String> with Map<String, List> in all
service method signatures and repository public APIs. Controllers pass
their MultiValueMap directly - no conversion needed since MultiValueMap
IS-A Map<String, List>.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MultiValuedMap is a class in spring framework core, not in a web library.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, misleading commit message. I still think it is an improvement as it is pure Java, which I think generally is preferably for repo and service layers. I will fix the commit message for now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagree. Why should a service layer be "pure Java"? Spring offers a lot of APIs that make perfect sense in a service layer. Or: it would be quite limiting to restrict a service layer to only JDK APIs.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. To be more clear, I the benefit is that the signature is not tied to a Spring interface - even if Spring is used elsewhere in the same (or lower) layer(s) for other reasons. And I guess the counter-argument is that MultiValueMap communicates intent more explicitly and is available either way.

TBH I have no strong feelings about this particular change, so I can drop it if you prefer it one way over the other.

Copy link
Copy Markdown
Collaborator

@georgweiss georgweiss left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in-line comment

@anderslindho anderslindho force-pushed the refactor/layer-isolation branch 3 times, most recently from 87d7588 to a6dbe48 Compare May 8, 2026 07:09
@anderslindho anderslindho marked this pull request as ready for review May 8, 2026 07:16
@anderslindho anderslindho marked this pull request as draft May 8, 2026 07:17
@anderslindho anderslindho force-pushed the refactor/layer-isolation branch 2 times, most recently from ce722ea to 575892c Compare May 8, 2026 07:51
Repositories must not know about HTTP. Replace all ResponseStatusException
throws in ChannelRepository, TagRepository, and PropertyRepository with
StorageException (for ES IO failures) or the existing typed domain exceptions
(ChannelValidationException for bad query windows). Add a StorageException
handler to V0ExceptionHandler mapping it to 500.

The NOT_FOUND status used in findById catch blocks was also wrong: those are
IO/connection failures, not 404s - replaced with StorageException.
…itory boundaries

Services should preferably not depend on Spring types. Replace
MultiValueMap<String, String> with Map<String, List<String>> in all
service method signatures and repository public APIs. Controllers pass
their MultiValueMap directly — no conversion needed since MultiValueMap
IS a Map<String, List<String>>.

Also replaces internal LinkedMultiValueMap usages in TagRepository and
PropertyRepository scroll loops, MetricsService metric-combination
generation, ChannelProcessorService.processAllChannels, and
ChannelFinderEpicsService with standard java.util equivalents.
@repository and @configuration must not be combined on the same class.
The @configuration annotation was causing repositories to participate in
Spring's configuration processing, which is incorrect. Remove it from
all three repositories and drop the now-unused import.
Add ChannelDto, TagDto, PropertyDto, SearchResultDto, ScrollDto under
web/v0/dto/ and corresponding static mappers under web/v0/mapper/.
All five controllers now map to/from DTOs; the service and domain
layers are unchanged.

This isolates the v0 API shape from the domain model so a future v1
API can evolve its own DTOs without touching service or repository code.
@anderslindho anderslindho force-pushed the refactor/layer-isolation branch from 575892c to 6c44109 Compare May 8, 2026 09:50
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented May 8, 2026

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 8, 2026

Overall Project 12.76% -5.76%
Files changed 6.26%

File Coverage
ChannelProcessorService.java 51.38% -4.59%
ChannelService.java 38.36% 🍏
PropertyService.java 22.57% -0.75%
MetricsService.java 22.39% -17.06%
PropertyRepository.java 0.56% -9.47%
TagRepository.java 0.56% -9.5%
ChannelRepository.java 0.26% -5.18%
ScrollDto.java 0%
PropertyDto.java 0%
SearchResultDto.java 0%
TagDto.java 0%
ChannelDto.java 0%
ChannelFinderEpicsService.java 0% -1.01%
ChannelScrollService.java 0% 🍏
TagController.java 0% -74.24%
ChannelController.java 0% -79.49%
PropertyController.java 0% -75%
ChannelProcessorController.java 0% -13.89%
ChannelScrollController.java 0% -81.25%
V0ExceptionHandler.java 0% -9.63%
ChannelMapper.java 0%
PropertyMapper.java 0%
TagMapper.java 0%
StorageException.java 0%

@anderslindho anderslindho marked this pull request as ready for review May 8, 2026 10:45
@@ -0,0 +1,12 @@
package org.phoebus.channelfinder.exceptions;

public class StorageException extends RuntimeException {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel like there should be a better name

RepositoryException?

import org.phoebus.channelfinder.entity.Channel;
import org.phoebus.channelfinder.web.v0.dto.ChannelDto;

public final class ChannelMapper {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a mapper class? Can we not do the mapping on the types themselves?

i.e.

Channel.toDto
ChannelDto.toChannel

?

Or the otherway round

Channel.fromDto
ChannelDto.fromChannel

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably because my mind was on MapStruct and I did not think of that 🫣

Channel.toDto sounds wrong - domain layer would depend on controller layer which is the wrong way around. So it'd have to be more like

ChannelDto.from
ChannelDto.toDomain

But it's not immediately clear to me if this will fit later on with v1 API. And it's to prep for that that I set up these (currently unnecessary) mappers in the first place.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I would rather the conversion happens inside of the service layer, since that is the mapping from controller layer to repository.

void tearDown() throws IOException {
ElasticConfigIT.teardown(esService);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These methods feel kind of weird to be in test file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants