This repository is an educational codebase that shows how to evolve a payment-processing app by applying SOLID principles and a few classic design patterns.
There are two major areas:
src/solid_principles/- Small, focused examples for each SOLID principle.
- Most principles are shown in
before.pyandafter.pyfiles to highlight improvements.
src/payment_service/- A larger "realistic" payment service that applies those ideas with protocols, builders, factories, validators, listeners, and decorators.
src/
solid_principles/
initial_code.py
single_responsability/
before.py
after.py
open_close/
before.py
after.py
liskov_substitution/
before.py
after.py
interfaces_segregation/
before.py
after.py
dependency_inversion/
before.py
after.py
payment_service/
main.py
service.py
builder.py
factory.py
logging_service.py
service_protocol.py
decorator_protocol.py
commons/
processors/
notifiers/
validators/
listeners/
loggers/
The default flow is orchestrated in src/payment_service/main.py:
- Create domain inputs (
CustomerData,PaymentData). - Use
PaymentServiceBuilderto wire dependencies. - Build a
PaymentServiceinstance. - Call
process_transaction.
Inside PaymentService.process_transaction:
- Build a
Requestobject. - Execute validator chain.
- Process payment with selected processor.
- Notify listeners (event style).
- Send customer notification (email/SMS).
- Persist transaction logs.
This gives you a single high-level entrypoint while keeping responsibilities separated.
commons defines the Pydantic models used throughout the project:
CustomerDataContactInfoPaymentDataandPaymentTypePaymentResponseRequest
If you understand these models, the rest of the code becomes much easier to follow.
Payment backends implement PaymentProcessorProtocol:
StripePaymentProcessorLocalPaymentProcessorOfflinePaymentProcessor
Some processors also implement:
RefundProcessorProtocolRecurringPaymentProcessorProtocol
This is a practical example of interface segregation and substitutability.
PaymentProcessorFactory chooses the correct processor based on PaymentData.type and currency.
Great place to study Open/Closed Principle tradeoffs.
ChainHandler defines the chain structure; CustomerHandler is one concrete step.
Current chain validates customer data and demonstrates composition of validation steps.
NotifierProtocol abstracts confirmation delivery.
EmailNotifier and SMSNotifier are interchangeable implementations.
ListenersManager keeps a list of listeners and broadcasts events.
This decouples event reactions from the core transaction path.
PaymentServiceBuilder constructs the service by selecting concrete dependencies and validating required parts before build().
This is the central wiring point for runtime behavior.
The solid_principles folder is your concept-first area:
initial_code.py: a monolithic baseline that mixes concerns.- Each principle folder:
before.py: code shape that violates or strains the principle.after.py: refactored code showing improvement.
Recommended reading order:
initial_code.pysingle_responsability/before.pythenafter.pyopen_close/before.pythenafter.pyliskov_substitution/before.pythenafter.pyinterfaces_segregation/before.pythenafter.pydependency_inversion/before.pythenafter.py
Using uv:
uv synccd src/payment_service
python main.pyNote: Stripe paths need environment variables like
STRIPE_API_KEY(andSTRIPE_PRICE_IDfor recurring setup).
-
Dependency Injection patterns in Python
- Move more wiring into a single composition root.
- Consider container-based DI once the project grows.
-
Typed protocols and static checks
- Add
mypyand enforce protocol compatibility across processors/notifiers/listeners.
- Add
-
Testing strategy by layer
- Unit tests for validators/factory/notifiers.
- Integration tests for
PaymentServiceorchestration. - Contract tests for processors.
-
Error handling and observability
- Replace
printcalls with structured logging. - Introduce domain exceptions for validation/business failures.
- Replace
-
Packaging/import hygiene
- Convert some absolute imports to package-relative imports so the app is easier to run from project root.
-
Extension exercises
- Add a new notifier (e.g., push notifications).
- Add another processor (e.g., PayPal).
- Add a
PaymentDataHandlerto chain validations. - Add persistence abstraction for logs/events.
commons: shared data shapes.processors: how payment happens.factory: chooses processor.validators: gate input correctness.notifiers: tell customer outcome.listeners: side effects on domain events.loggers: persistence/trace.service: orchestration.builder: configuration and assembly.
If you start with that model, most files in this project will feel intuitive very quickly.