Design.md
Markdown herunterladenDesign der Umfrageverwaltung (Survey Management)
Überblick
Die Funktion Umfrageverwaltung stellt eine RESTful API zum Erstellen, Konfigurieren und Verwalten von Online-Umfragen bereit. Das System folgt einer zustandslosen Microservice-Architektur auf Basis von Spring Boot, sodass Umfrage-Administrator:innen den vollständigen Lebenszyklus einer Umfrage abbilden können – inklusive Erstellung, Fragenverwaltung, Abruf und Löschung.
Das Design betont Datenintegrität, Validierung und Skalierbarkeit durch zustandslose Operationen. Alle Umfragedaten werden mittels JPA/Hibernate persistiert.
Architektur
Grobarchitektur
Das System folgt dem Microservice-Architekturpattern:
┌─────────────────────────────────────────────┐
│ Client Applications │
│ (Web, Mobile, Third-party services) │
└────────────────┬────────────────────────────┘
│ HTTP/REST
▼
┌─────────────────────────────────────────────┐
│ Online Survey Backend (Spring Boot) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ REST API Layer (Controllers) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Business Logic Layer (Services) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Data Access Layer (Repositories) │ │
│ └──────────────┬───────────────────────┘ │
│ │ │
│ ┌──────────────▼───────────────────────┐ │
│ │ Domain Model (Entities) │ │
│ └──────────────────────────────────────┘ │
└────────────────┬────────────────────────────┘
│ JDBC
▼
┌─────────────────────────────────────────────┐
│ Database (H2/PostgreSQL) │
└─────────────────────────────────────────────┘
Technologie-Stack
- Framework: Spring Boot 3.x
- Persistenz: Spring Data JPA mit Hibernate
- Datenbank: H2 (Entwicklung), PostgreSQL (produktionstauglich)
- Validierung: Bean Validation (JSR-303)
- Testing: JUnit 5, Spring Boot Test, jqwik (Property-Based Testing)
Designentscheidungen
- Separation of Concerns: Jede Schicht hat klar getrennte Verantwortlichkeiten; entkoppelte Modelle und Mapper übersetzen zwischen den Schichten.
- Zustandslose Kommunikation: Jede Anfrage enthält alle notwendigen Informationen und ermöglicht horizontale Skalierung.
- UUID-Identifikatoren: Alle Entitäten nutzen UUIDv4 für global eindeutige, nicht-sequentielle Identifikatoren.
- Cascade-Operationen: Das Löschen einer Umfrage kaskadiert zu Fragen und Antwortoptionen, um Datenkonsistenz sicherzustellen.
- Validation-First-Ansatz: Umfassende Validierung auf Entitäts- und Service-Ebene nach etablierten Patterns.
- RESTful API Design: Ressourcenbasierte URLs mit korrekten HTTP-Methoden und Statuscodes.
Komponenten und Schnittstellen
REST-Controller
SurveyController
POST /api/surveys– Neue Umfrage erstellenGET /api/surveys/{surveyId}– Umfragedetails abrufenDELETE /api/surveys/{surveyId}– Umfrage löschenPOST /api/surveys/{surveyId}/questions– Frage zur Umfrage hinzufügenDELETE /api/surveys/{surveyId}/questions/{questionId}– Frage löschen
Service-Schicht
SurveyService
- Management des Umfrage-Lebenszyklus
- Validierung von Business-Regeln
- Transaktionskoordination
QuestionService
- Fragenverwaltung innerhalb von Umfragen
- Handling von Antwortoptionen
- Validierung der Reihenfolge
Repository-Schicht
SurveyRepository (erweitert JpaRepository)
- Basis-CRUD-Operationen für Umfragen
- Custom Queries zum Abruf von Umfragen inklusive Fragen
QuestionRepository (erweitert JpaRepository)
- CRUD-Operationen für Fragen
- Queries zur Validierung von Ordnungsnummern
AnswerOptionRepository (erweitert JpaRepository)
- Verwaltung von Antwortoptionen
- Validierung der Eindeutigkeit interner Werte
Datenmodelle
Die Datenmodelle entsprechen dem semantischen Datenmodell und implementieren die Entitäten Survey, Question und AnswerOption für die Umfrageverwaltungsfunktion.
Survey-Entität
@Entity
@Table(name = "survey")
public class Survey {
@Id
@Column(name = "survey_id")
private UUID surveyId;
@NotBlank
@Size(max = 200)
@Column(name = "title", nullable = false)
private String title;
@Size(max = 2000)
@Column(name = "description")
private String description;
@NotBlank
@Size(max = 100)
@Column(name = "created_by", nullable = false)
private String createdBy;
@Column(name = "start_date")
private LocalDate startDate;
@Column(name = "end_date")
private LocalDate endDate;
@CreationTimestamp
@Column(name = "created_at", nullable = false)
private Instant createdAt;
@OneToMany(mappedBy = "survey", cascade = CascadeType.ALL, orphanRemoval = true)
@OrderBy("orderNumber ASC")
private List<Question> questions = new ArrayList<>();
}
Question-Entität
@Entity
@Table(name = "question")
public class Question {
@Id
@Column(name = "question_id")
private UUID questionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "survey_id", nullable = false)
private Survey survey;
@NotNull
@Min(1)
@Column(name = "order_number", nullable = false)
private Integer orderNumber;
@NotBlank
@Size(max = 1000)
@Column(name = "question_text", nullable = false)
private String questionText;
@Enumerated(EnumType.STRING)
@NotNull
@Column(name = "question_type", nullable = false)
private QuestionType questionType;
@NotNull
@Column(name = "is_mandatory", nullable = false)
private Boolean isMandatory;
@OneToMany(mappedBy = "question", cascade = CascadeType.ALL, orphanRemoval = true)
private List<AnswerOption> answerOptions = new ArrayList<>();
}
AnswerOption-Entität
@Entity
@Table(name = "answer_option")
public class AnswerOption {
@Id
@Column(name = "option_id")
private UUID optionId;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "question_id", nullable = false)
private Question question;
@NotBlank
@Size(max = 200)
@Column(name = "option_text", nullable = false)
private String optionText;
@NotBlank
@Pattern(regexp = "^[A-Za-z0-9_-]+$")
@Size(max = 100)
@Column(name = "value", nullable = false)
private String value;
@Column(name = "is_other_option")
private Boolean isOtherOption = false;
}
QuestionType-Enum
public enum QuestionType {
RATING("Rating"),
SINGLE_CHOICE("SingleChoice"),
MULTIPLE_CHOICE("MultipleChoice"),
OPEN("Open");
private final String value;
QuestionType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
}
Entitätsbeziehungen
- Survey → Questions (One-to-Many, CASCADE ALL)
- Question → AnswerOptions (One-to-Many, CASCADE ALL)
- Bidirektionale Beziehungen mit passenden Foreign-Key-Constraints
- Spaltennamen und Constraints entsprechen den Spezifikationen des semantischen Datenmodells
Korrektheits-Properties
Eine Property ist eine Eigenschaft oder ein Verhalten, das über alle gültigen Ausführungen eines Systems hinweg gelten soll – im Kern eine formale Aussage darüber, was das System tun muss. Properties bilden die Brücke zwischen menschenlesbaren Spezifikationen und maschinell überprüfbaren Korrektheitsgarantien.
Nach der Analyse der Akzeptanzkriterien lassen sich mehrere Properties konsolidieren, um Redundanzen zu reduzieren und gleichzeitig eine umfassende Validierungsabdeckung sicherzustellen:
Property 1: Umfrageerstellung mit gültigen Daten ist erfolgreich Für beliebige gültige Umfragedaten (nicht-leerer Titel, gültiger Ersteller, korrekte Datumsreihenfolge) muss das Erstellen einer Umfrage erfolgreich sein und eine eindeutige UUID zurückgeben. Validiert: Anforderungen 1.1, 1.2, 1.5
Property 2: Ungültige Umfragedaten werden mit aussagekräftigen Fehlern abgelehnt Für beliebige ungültige Umfragedaten (leerer/Whitespace-Titel, Enddatum vor Startdatum, fehlende Pflichtfelder) muss die Erstellung abgelehnt werden und spezifische Fehlermeldungen liefern, die die Validierungsursache erklären. Validiert: Anforderungen 1.3, 1.4, 6.1, 6.2
Property 3: Erstellung einer Frage erhält alle angegebenen Attribute Für beliebige gültige Fragendaten, die einer existierenden Umfrage hinzugefügt werden, muss die gespeicherte Frage exakt die angegebene Ordnungsnummer, den Text, den Typ, das Pflicht-Flag sowie alle gelieferten Antwortoptionen mit Anzeigetext und internem Wert enthalten. Validiert: Anforderungen 2.1, 2.2
Property 4: Fragenvalidierung erzwingt Business-Regeln Für beliebige Versuche, Fragen zu erstellen, die Business-Regeln verletzen (doppelte Ordnungsnummern, zu wenige Antwortoptionen für Choice-/Rating-Fragen, doppelte interne Werte), muss das System die Erstellung ablehnen und spezifische Fehlermeldungen zurückgeben. Validiert: Anforderungen 2.3, 2.4, 2.5, 6.3
Property 5: Umfrageabruf liefert vollständige und geordnete Daten Für beliebige existierende Umfragen muss der Abruf alle Umfragemetadaten, Fragen in aufsteigender Reihenfolge nach Ordnungsnummer, alle Antwortoptionen (Anzeigetext und interner Wert), den Erstellungszeitpunkt sowie alle notwendigen Identifikatoren für Folgeoperationen enthalten. Validiert: Anforderungen 3.1, 3.3, 3.4, 3.5, 7.4
Property 6: Anfragen auf nicht existierende Ressourcen liefern passende Fehler Für beliebige Anfragen mit ungültigen Identifikatoren (nicht existierende Survey-ID, Question-ID) muss das System Not-Found-Fehler zurückgeben, die klar benennen, welche Ressource nicht gefunden wurde. Validiert: Anforderungen 3.2, 4.3, 5.2, 6.4
Property 7: Löschen einer Frage entfernt die Frage und kaskadiert zu Antwortoptionen Für beliebige gültige Löschanfragen für Fragen müssen die Frage und alle zugehörigen Antwortoptionen dauerhaft entfernt werden; eine Bestätigung wird zurückgegeben, aber keine gelöschten Daten. Validiert: Anforderungen 4.1, 4.4, 4.5
Property 8: Cross-Survey-Operationen auf Fragen werden abgelehnt Für beliebige Versuche, eine Frage zu löschen, wobei die Question-ID zu einer anderen Umfrage gehört als in der URL angegeben, muss das System die Operation mit einem Validierungsfehler ablehnen. Validiert: Anforderungen 4.2
Property 9: Umfragelöschung kaskadiert vollständig Für beliebige gültige Löschanfragen für Umfragen müssen die Umfrage, alle Fragen, alle Antwortoptionen sowie alle zugehörigen Responses dauerhaft entfernt werden; eine Bestätigung wird zurückgegeben, aber keine gelöschten Daten. Validiert: Anforderungen 5.1, 5.3, 5.4, 5.5
Property 10: Mehrere Validierungsfehler werden aggregiert Für beliebige Anfragen mit mehreren Validierungsproblemen müssen alle Validierungsfehler in einer einzigen Response zurückgegeben werden. Validiert: Anforderungen 6.5
Property 11: Operationen erfordern korrekte Identifikatoren Für beliebige mehrstufige Operationssequenzen muss jede Anfrage die passenden Identifikatoren (Survey-ID, Question-ID) explizit enthalten. Validiert: Anforderungen 7.2
Property 12: Fehlerantworten sind in sich geschlossen Für beliebige Fehlerbedingungen muss die Fehlerantwort vollständige Informationen enthalten, ohne sich auf Kontext aus vorherigen Requests zu stützen. Validiert: Anforderungen 7.5
Property 13: Textdaten werden konsistent normalisiert Für beliebige Texteingaben mit führenden oder nachgestellten Leerzeichen muss der gespeicherte Wert getrimmt werden. Validiert: Anforderungen 8.1
Property 14: Validierung und Standardisierung von Datumsformaten Für beliebige Datumseingaben muss das System das Format validieren und Daten im ISO-8601-Format (YYYY-MM-DD) speichern. Validiert: Anforderungen 8.2
Property 15: Validierung des Formats interner Werte Für beliebige interne Werte von Antwortoptionen darf das System ausschließlich alphanumerische Zeichen, Bindestriche und Unterstriche akzeptieren. Validiert: Anforderungen 8.3
Property 16: Konsistenz von Identifikator- und Timestamp-Formaten Für beliebige Entitätserstellungen müssen generierte Identifikatoren dem UUID-Format entsprechen und Erstellungs-Timestamps im ISO-8601-UTC-Format vorliegen. Validiert: Anforderungen 8.4, 8.5
Fehlerbehandlung
Strategie für HTTP-Statuscodes
Gemäß den Architekturleitlinien:
- 400 Bad Request: Ungültige Eingaben, Validierungsfehler, Verletzung von Business-Regeln
- 404 Not Found: Angeforderte Ressource existiert nicht
- 409 Conflict: Operation kollidiert mit dem aktuellen Zustand (z. B. doppelte Ordnungsnummern)
- 422 Unprocessable Entity: Request ist syntaktisch gültig, kann aber wegen Business-Regeln nicht verarbeitet werden
- 500 Internal Server Error: Unerwartete Systemfehler
Format der Validierungsfehlerantwort
{
"timestamp": "2024-01-15T10:30:00Z",
"status": 400,
"error": "Bad Request",
"message": "Validation failed",
"path": "/api/surveys",
"correlationId": "abc123-def456-ghi789",
"validationErrors": [
{
"field": "title",
"rejectedValue": "",
"message": "Title cannot be empty or contain only whitespace"
},
{
"field": "endDate",
"rejectedValue": "2024-01-01",
"message": "End date must be after start date"
}
]
}
Strategie für Exception-Handling
- Eigene Exception-Klassen für unterschiedliche Fehlertypen
- Globaler Exception-Handler via
@ControllerAdvice - Konsistentes Error-Response-Format über alle Endpoints
- Correlation IDs für Troubleshooting
- Detailliertes Logging mit Correlation IDs, ohne sensible Daten offenzulegen
- Keine Preisgabe interner Systemdetails, Datenbankstrukturen oder sensibler Informationen
Teststrategie
Dualer Testansatz
Die Teststrategie kombiniert Unit-Tests und Property-Based Tests für umfassende Abdeckung:
- Unit-Tests verifizieren konkrete Beispiele, Edge Cases und Integrationspunkte zwischen Komponenten.
- Property-Based Tests verifizieren universelle Properties, die über alle Inputs hinweg gelten sollen, mithilfe der jqwik-Bibliothek.
- Zusammen liefern sie eine hohe Abdeckung: Unit-Tests finden konkrete Bugs, Property-Tests verifizieren allgemeine Korrektheit.
Anforderungen an Unit-Tests
Unit-Tests decken ab:
- Konkrete Beispiele, die das korrekte Verhalten je Endpoint demonstrieren
- Integration zwischen Controller-, Service- und Repository-Schicht
- Fehlerbehandlungsszenarien mit bekannten Inputs
- Validierung von Datenbank-Constraints
- JSON-Serialisierung/Deserialisierung
Anforderungen an Property-Based Tests
Property-Based Testing nutzt die Bibliothek jqwik für Java und wird so konfiguriert, dass mindestens 100 Iterationen pro Property-Test ausgeführt werden. Jeder Property-Test wird mit einem Kommentar getaggt, der explizit auf die Korrektheits-Property aus diesem Dokument referenziert, im Format: **Feature: survey-management, Property {number}: {property_text}**
Jede oben aufgeführte Korrektheits-Property wird durch genau einen Property-Based Test implementiert, der zufällige gültige und ungültige Inputs generiert, um das spezifizierte Verhalten universell zu verifizieren.
Strategie zur Testdatengenerierung
- Smart Generators: Generatoren, die Inputs intelligent auf gültige Wertebereiche einschränken
- Edge-Case-Abdeckung: Grenzwerte, leere Collections und Limit-Fälle
- Generierung ungültiger Inputs: Verschiedene Klassen ungültiger Daten zur Validierung der Fehlermeldungen
- Realistische Daten: Sinnvolle Umfragetitel, Fragen und Antwortoptionen für bessere Lesbarkeit der Tests
Konfiguration des Test-Frameworks
- **JUnit 5** für Unit-Tests mit Spring-Boot-Test-Integration
- **jqwik** für Property-Based Testing mit Custom Generators
- **TestContainers** für Integrationstests mit einer echten Datenbank
- **MockMvc** für REST-API-Tests
- **AssertJ** für Fluent Assertions
Der Testansatz stellt sicher, dass sowohl konkrete Szenarien als auch das allgemeine Systemverhalten gründlich validiert werden – und schafft damit Vertrauen in Korrektheit und Robustheit des Umfrageverwaltungssystems.