Part 4

Design Principles & Patterns

How SOLID principles guide the system's structure, and which design patterns are applied to solve recurring architectural challenges.

SOLID

Applied SOLID Principles

Three of the five SOLID principles and their concrete application in Abokabot.

S

Single Responsibility Principle

Each service in Abokabot is scoped to one bounded context. The Ticketing Service handles ticket lifecycle only; fare calculation is delegated to a dedicated FareEngine class. This means a regulatory change to fare rules requires modifying only FareEngine without touching TicketingService, reducing the blast radius of changes and simplifying unit testing.

O

Open/Closed Principle

The FareEngine uses a strategy pattern internally, making it open for extension but closed for modification. When a new passenger category (e.g., "military discount") is introduced, a new FareStrategy implementation is added without modifying existing strategy code. The engine selects the appropriate strategy at runtime based on user role, keeping the core stable.

D

Dependency Inversion Principle

High-level services depend on abstractions, not concretions. The TicketingService depends on a CardValidationPort interface, not directly on MengedApiClient. This decouples the business logic from the external payment system. During testing, a MockMengedAdapter satisfies the interface, enabling full integration tests without real Menged API calls. If Menged is replaced by another payment system, only the adapter changes.

Design Patterns

Applied Patterns

Singleton and Observer patterns: the problem each solves and how they integrate into the architecture.

Creational

Singleton Pattern

Problem

System-wide configuration (API keys, timeout thresholds, Menged endpoint URLs) was scattered across multiple service files, causing inconsistencies when values were updated and making environment-specific deployments error-prone.

Solution

A SystemConfig Singleton class is instantiated once at application startup, reading from environment variables. All services obtain configuration by calling SystemConfig.getInstance(). This guarantees a single source of truth, prevents conflicting config values, and enables centralized reload logic without restarting services.

Fit in Architecture

Sits at the infrastructure layer, consumed by the API Gateway and all microservices. In a containerized deployment, each container instance has its own Singleton, which is correct behavior since config is per-process, not per-request.

Behavioral

Observer Pattern

Problem

Multiple system components needed to react when a boarding event occurred: the audit log needed to record it, the operator dashboard needed a real-time count update, and a fraud detection module needed to analyze the event. Tightly coupling TicketingService to all these consumers created a god-class anti-pattern.

Solution

The TicketingService emits a BoardingEvent to an internal event bus. The AuditLogger, OperatorDashboardNotifier, and FraudDetector are registered as observers. Each reacts independently when a boarding event is published. Adding a new observer (e.g., analytics service) requires no modification to TicketingService.

Fit in Architecture

Implemented as a lightweight in-process event emitter. In the cloud deployment, this scales to a message broker (e.g., RabbitMQ) for cross-service observers, maintaining the same decoupled contract without architectural rework.

Diagrams

Pattern Class Diagrams

Structural representation of the Singleton and Observer implementations.

Figure 5: Singleton Pattern — SystemConfig shared across all services.
Figure 6: Observer Pattern — TicketingService publishes boarding events; observers react independently.