Skip to content

Commit 91efea0

Browse files
authored
Merge pull request #60 from MEITREX/documentation
Documentation
2 parents ac08c1d + a5790cc commit 91efea0

34 files changed

+335
-3
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,64 @@ The API documentation can be found in the wiki in the [API docs](api.md).
3434

3535
The API is available at `/graphql` and the GraphiQL interface is available at `/graphiql`.
3636

37+
## Package Structure – `de.unistuttgart.iste.meitrex.gamification_service.service`
38+
39+
The business logic of the Gamification Service is organized in the package
40+
`de.unistuttgart.iste.meitrex.gamification_service.service`.
41+
To ensure a clear architecture and separation of concerns, this package is divided into several **subpackages**:
42+
43+
| Package | Purpose |
44+
|-------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
45+
| `de.unistuttgart.iste.meitrex.gamification_service.service.internal` | Contains **implementations of business logic** that are **not directly exposed via GraphQL**. These classes encapsulate internal application logic and are typically resolvers or event listeners. |
46+
| `de.unistuttgart.iste.meitrex.gamification_service.service.functional` | Contains **functional, idempotent utility functions** that have no side effects. These classes and methods are usually stateless and can safely be reused throughout the application. |
47+
| `de.unistuttgart.iste.meitrex.gamification_service.service.reactive` | Contains **event listeners** that extend `de.unistuttgart.iste.meitrex.gamification_service.events.internal.AbstractInternalListener`. These listeners react to **application events**, especially **external events that have been translated into internal events**. |
48+
49+
### Implementation Guidelines
50+
- **`internal`**: Use this package for services or components that implement complex business rules or calculations. These components may be used by various GraphQL resolvers or event listeners, but they should not handle transport or DTO transformations.
51+
- **`functional`**: Use this package for small, easily testable functions that perform calculations, formatting, or transformations without modifying the application state.
52+
- **`reactive`**: Listeners in this package are triggered via Spring Application Events and handle reactions to internal event streams. Typically, these correspond to **external events** that have been converted into **internal events** by a separate adapter layer.
53+
54+
## Processing External Events
55+
56+
External events are processed in three steps to ensure durability:
57+
58+
1. **Reception & Mapping**
59+
External events are received through Dapr pub/sub endpoints by listeners extending
60+
`de.unistuttgart.iste.meitrex.gamification_service.dapr.AbstractExternalListener<T>`.
61+
These map incoming `CloudEvent<T>` payloads to `PersistentEvent` objects and pass them to the `IEventPublicationService`.
62+
63+
2. **Persistence & Publication**
64+
The `DefaultEventPublicationService` persists each `PersistentEvent` and publishes a corresponding `InternalEvent` after the surrounding transaction commits.
65+
Duplicate events (based on sequence numbers) are ignored.
66+
67+
3. **Internal Event Handling**
68+
Internal events (`de.unistuttgart.iste.meitrex.gamification_service.events.internal.InternalEvent`) are processed asynchronously by listeners extending
69+
`de.unistuttgart.iste.meitrex.gamification_service.events.internal.AbstractInternalListener<U,V>`.
70+
These listeners:
71+
- Load the persistent event from the database,
72+
- Track processing status and retry attempts,
73+
- Execute business logic in `doProcess(U persistentEvent)`,
74+
- Distinguish between transient and non-transient failures.
75+
76+
**Example Flow**:
77+
`CloudEvent<T>``AbstractExternalListener``PersistentEvent` stored → `InternalEvent` published → `AbstractInternalListener` processes domain logic.
78+
79+
### Implementing a Custom Internal Listener
80+
81+
To react to new internal events, developers must:
82+
83+
1. **Create a `PersistentEvent` subclass** representing the data structure to be stored for the new external event.
84+
2. **Extend `AbstractExternalListener<T>`** to receive and map the external event to the new `PersistentEvent`.
85+
3. **Register the new event type** in `DefaultEventPublicationService` by adding a mapping from the new `PersistentEvent` class to a function that saves it and creates a corresponding `InternalEvent`.
86+
4. **Create an `InternalEvent` subclass** that references the persisted event’s UUID.
87+
5. **Implement a new listener** by extending `AbstractInternalListener<YourPersistentEvent, YourInternalEvent>` and overriding:
88+
- `getName()` with a unique, stable identifier,
89+
- `doProcess(...)` with the actual business logic to run when the event is processed.
90+
6. Optionally throw `TransientEventListenerException` for retryable errors or `NonTransientEventListenerException` for permanent failures.
91+
92+
After these steps, the new internal listener will automatically react to published internal events of the specified type.
93+
94+
3795
## Get started
3896
A guide how to start development can be
3997
found in the [wiki](https://meitrex.readthedocs.io/en/latest/dev-manuals/backend/get-started.html).

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/AbstractInternalListener.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.unistuttgart.iste.meitrex.gamification_service.events.internal;
22

3+
import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable;
34
import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent;
45
import de.unistuttgart.iste.meitrex.gamification_service.events.repository.IPersistentEventRepository;
56
import de.unistuttgart.iste.meitrex.gamification_service.events.repository.IPersistentEventStatusRepository;
@@ -10,6 +11,24 @@
1011
import java.nio.charset.StandardCharsets;
1112
import java.util.*;
1213

14+
/**
15+
* Abstract base class for internal event listeners in the gamification service. This listener implements generic
16+
* functionality for processing {@link PersistentEvent} and their corresponding {@link InternalEvent}s It handles:
17+
* (1) fetching the associated persistent event from the repository, (2) tracking and updating processing status
18+
* for retries and failures, (3) delegating the actual business logic to subclasses via {@link #doProcess(PersistentEvent)},
19+
* (4) retry logic with configurable max attempt count. Any concrete listener is expected to be executed within a
20+
* transactional context (e.g., managed by Spring), otherwise, event status updates will not be persisted.
21+
*
22+
* To implement a custom listener for a specific type of event, create a subclass overriding the following methods:
23+
* (1) {@link #getName()} - must return a unique and stable name.
24+
* (2) {@link #doProcess(U persistentEvent))} - contains the actual processing logic. In case of an unrecoverable error,
25+
* an implementation should throw a {@link NonTransientEventListenerException}. Otherwise, {@link TransientEventListenerException}
26+
* should be thrown. In the latter case, the base class logic takes care of updating the event status and retrying.
27+
*
28+
* @param <U> the type of the persistent event associated with the internal event.
29+
* @param <V> the type of the internal event to be processed.
30+
* @author Philiipp Kunz
31+
*/
1332
public abstract class AbstractInternalListener<U extends PersistentEvent, V extends InternalEvent> {
1433

1534

@@ -43,6 +62,13 @@ public final UUID getListenerUUID() {
4362
return UUID.nameUUIDFromBytes(getName().getBytes(StandardCharsets.UTF_8));
4463
}
4564

65+
@Loggable(
66+
inLogLevel = Loggable.LogLevel.INFO,
67+
exitLogLevel = Loggable.LogLevel.DEBUG,
68+
exceptionLogLevel = Loggable.LogLevel.WARN,
69+
logExecutionTime = false,
70+
logExit = false
71+
)
4672
public void process(V internalEvent) {
4773
assureIsValid(internalEvent);
4874
process(fetchPersistentEvent(internalEvent));

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/InternalEvent.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
import org.springframework.context.*;
99

1010

11+
/**
12+
* Each {@code InternalEvent} corresponds to a {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent}
13+
* stored in the database and is represented as a Spring {@link org.springframework.context.ApplicationEvent}. These events
14+
* can be published within the application context and are typically consumed by {@link AbstractInternalListener} implementations.
15+
* Every {@code InternalEvent} is identified by a unique {@link java.util.UUID}, which references the corresponding
16+
* {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent} instance.
17+
*
18+
* @author Philipp Kunz
19+
*/
1120
@Getter
1221
public abstract class InternalEvent extends ApplicationEvent {
1322

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/NonTransientEventListenerException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package de.unistuttgart.iste.meitrex.gamification_service.events.internal;
22

3+
/**
4+
* Exception indicating a non-recoverable error during the processing of an internal event. Throwing this exception
5+
* signals hat the event processing has failed permanently and should not be retried by the surrounding infrastructure.
6+
*
7+
* @see AbstractInternalListener
8+
*/
39
public class NonTransientEventListenerException extends InternalEventListenerException {
410

511
public NonTransientEventListenerException() {

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/internal/TransientEventListenerException.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
package de.unistuttgart.iste.meitrex.gamification_service.events.internal;
22

3+
/**
4+
* Exception indicating a recoverable error during the processing of an internal event. Throwing this exception
5+
* signals hat the event processing has permanently and should be retried by the surrounding infrastructure.
6+
*
7+
* @see AbstractInternalListener
8+
*/
39
public class TransientEventListenerException extends InternalEventListenerException {
410

511
public TransientEventListenerException() {

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/persistent/PersistentEvent.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@
99
import java.util.List;
1010
import java.util.UUID;
1111

12+
/**
13+
* Base class for all persistently stored events within the gamification service. A PersistentEvent represents an
14+
* externally received event that is stored permanently in the database. Each event has a unique UUID as its primary key,
15+
* an optional message sequence number, and a timestamp indicating the time it was received. Each PersistentEvent can
16+
* have multiple associated processing statuses, which are managed in the embedded PersistentEventStatus class. Such a
17+
* status represents the processing state of an event for a specific listener. This allows the progress and outcome of
18+
* event processing to be tracked per listener, including the current attempt count, the maximum number of allowed retries,
19+
* and the timestamp of the last processing attempt. The class is declared abstract because concrete event types are
20+
* defined through subclasses. These subclasses may contain additional domain-specific information required for further
21+
* processing.
22+
*
23+
* @author Philipp Kunz
24+
*/
1225
@Getter
1326
@Setter
1427
@SuperBuilder

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/DefaultEventPublicationService.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package de.unistuttgart.iste.meitrex.gamification_service.events.publication;
22

3+
import de.unistuttgart.iste.meitrex.gamification_service.aspects.logging.Loggable;
34
import de.unistuttgart.iste.meitrex.gamification_service.events.PersistentMediaRecordWorkedOnEvent;
45
import de.unistuttgart.iste.meitrex.gamification_service.events.internal.*;
56
import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.*;
@@ -17,6 +18,27 @@
1718
import java.util.UUID;
1819
import java.util.function.Function;
1920

21+
/**
22+
* Default implementation of {@link IEventPublicationService} that persists incoming persistent events,
23+
* ensures that duplicate events are not processed, and publishes corresponding internal events
24+
* after a successful transaction commit.
25+
*
26+
* This service uses a type-to-handler map to delegate the persistence and conversion logic
27+
* for different event types. The {@link org.springframework.transaction.support.TransactionSynchronizationManager}
28+
* is used to ensure that publication happens only after the surrounding database transaction commits successfully.
29+
*
30+
* Duplicate detection is applied only to events that implement the {@link de.unistuttgart.iste.meitrex.gamification_service.events.persistent.ISequenced}
31+
* interface. For these events, the message sequence number is checked against the database to prevent reprocessing
32+
* of duplicates. All other events are always treated as new and published unconditionally.
33+
*
34+
* To support additional event types, implement a corresponding persistence and conversion method (e.g. {@code saveMyEventType}),
35+
* and register it in the {@code handlerMap} inside the constructor, mapping the new persistent event class
36+
* to the handler function. This allows the service to process and publish the new event type without modifying
37+
* the core publishing logic.
38+
*
39+
* @author Philipp Kunz
40+
*/
41+
2042
@Slf4j
2143
@Component
2244
class DefaultEventPublicationService implements IEventPublicationService {
@@ -109,6 +131,13 @@ public DefaultEventPublicationService(
109131
}
110132

111133
@Override
134+
@Loggable(
135+
inLogLevel = Loggable.LogLevel.DEBUG,
136+
exitLogLevel = Loggable.LogLevel.DEBUG,
137+
exceptionLogLevel = Loggable.LogLevel.WARN,
138+
logArgs = false,
139+
logExecutionTime = false
140+
)
112141
public void saveCommitAndPublishIfNew(PersistentEvent persistentEvent) {
113142
if(!this.handlerMap.containsKey(persistentEvent.getClass())) {
114143
throw new IllegalArgumentException(ERR_MSG_UNSUPPORTED_EVENT_TYPE);

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/events/publication/IEventPublicationService.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,23 @@
22

33
import de.unistuttgart.iste.meitrex.gamification_service.events.persistent.PersistentEvent;
44

5+
/**
6+
* Defines the contract for publishing internal application events. Implementations are responsible for persisting
7+
* events, ensuring that duplicate events are not processed, and publishing new events only after the surrounding
8+
* transaction has been successfully committed.
9+
*
10+
* @author Philipp Kunz
11+
*/
512
public interface IEventPublicationService {
613

14+
15+
/**
16+
* Saves the given event if it has not been processed before and publishes it after the current transaction
17+
* commits successfully as an instance of {@link de.unistuttgart.iste.meitrex.gamification_service.events.internal.InternalEvent}.
18+
*
19+
* @param persistentEvent the event to persist and publish (must not be {@code null})
20+
* @throws IllegalArgumentException if the event type is unsupported or the event is invalid
21+
*/
722
void saveCommitAndPublishIfNew(PersistentEvent persistentEvent);
823

924
}

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultKeycloakClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public DefaultKeycloakClient(
6868

6969
@Override
7070
@Loggable(
71-
inLogLevel = Loggable.LogLevel.INFO,
71+
inLogLevel = Loggable.LogLevel.DEBUG,
7272
exitLogLevel = Loggable.LogLevel.DEBUG,
7373
exceptionLogLevel = Loggable.LogLevel.WARN,
7474
logExecutionTime = false

src/main/java/de/unistuttgart/iste/meitrex/gamification_service/keycloak/DefaultUserConfiguration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public DefaultUserConfiguration(IKeycloakClient keycloakClient) {
3838

3939
@Override
4040
@Loggable(
41-
inLogLevel = Loggable.LogLevel.INFO,
41+
inLogLevel = Loggable.LogLevel.DEBUG,
4242
exitLogLevel = Loggable.LogLevel.DEBUG,
4343
exceptionLogLevel = Loggable.LogLevel.WARN,
4444
logExecutionTime = false

0 commit comments

Comments
 (0)