Polanya Desain Python untuk Arsitektur Bersih
Bangun aplikasi Python yang dapat dipelihara dengan pola desain SOLID
Clean Architecture telah merevolusi cara pengembang membangun aplikasi yang skalabel dan dapat dipelihara dengan menekankan pemisahan kepentingan dan manajemen ketergantungan.
Di Python, prinsip-prinsip ini berpadu dengan sifat dinamis bahasa untuk menciptakan sistem yang fleksibel dan dapat diuji yang berkembang sesuai dengan kebutuhan bisnis tanpa menjadi utang teknis.

Memahami Clean Architecture dalam Python
Clean Architecture, yang diperkenalkan oleh Robert C. Martin (Uncle Bob), mengorganisasi perangkat lunak menjadi lapisan-lapisan konsentris di mana ketergantungan menunjuk ke dalam logika bisnis inti. Pola arsitektur ini memastikan bahwa aturan bisnis kritis aplikasi tetap independen dari kerangka kerja, database, dan layanan eksternal.
Filosofi Inti
Prinsip dasar sederhana namun kuat: logika bisnis tidak boleh bergantung pada infrastruktur. Entitas domain, kasus penggunaan, dan aturan bisnis Anda harus berfungsi tanpa peduli apakah Anda menggunakan PostgreSQL atau MongoDB, FastAPI atau Flask, AWS atau Azure.
Di Python, filosofi ini sangat cocok dengan sifat “duck typing” dan pemrograman berbasis protokol dari bahasa, memungkinkan pemisahan bersih tanpa upacara yang diperlukan dalam bahasa bertipe statis.
Empat Lapisan Clean Architecture
Lapisan Entitas (Domain): Objek bisnis murni dengan aturan bisnis yang berlaku di seluruh perusahaan. Ini adalah POJO (Plain Old Python Objects) tanpa ketergantungan eksternal.
Lapisan Kasus Penggunaan (Aplikasi): Aturan bisnis spesifik aplikasi yang mengatur alur data antara entitas dan layanan eksternal.
Lapisan Penyesuaian Antarmuka: Mengubah data antara format yang paling nyaman untuk kasus penggunaan dan entitas, dan format yang diperlukan oleh pihak eksternal.
Lapisan Kerangka Kerja & Pengemudi: Semua detail eksternal seperti database, kerangka kerja web, dan API eksternal.
Prinsip SOLID dalam Python
Prinsip SOLID membentuk fondasi dari arsitektur bersih. Mari kita eksplorasi bagaimana setiap prinsip muncul dalam Python. Untuk tinjauan menyeluruh tentang pola desain dalam Python, lihat Panduan Pola Desain Python.
Prinsip Tanggung Jawab Tunggal (SRP)
Setiap kelas harus memiliki satu alasan untuk berubah:
# Buruk: Banyak tanggung jawab
class UserManager:
def create_user(self, user_data):
# Membuat pengguna
pass
def send_welcome_email(self, user):
# Mengirim email
pass
def log_creation(self, user):
# Mencatat ke file
pass
# Baik: Tanggung jawab terpisah
class UserService:
def __init__(self, repository, email_service, logger):
self.repository = repository
self.email_service = email_service
self.logger = logger
def create_user(self, user_data):
user = User(**user_data)
self.repository.save(user)
self.email_service.send_welcome(user)
self.logger.info(f"Pengguna dibuat: {user.id}")
return user
Prinsip Terbuka/Tertutup (OCP)
Entitas perangkat lunak harus terbuka untuk ekstensi tetapi tertutup untuk modifikasi:
from abc import ABC, abstractmethod
from typing import Protocol
# Menggunakan Protocol (Python 3.8+)
class PaymentProcessor(Protocol):
def process_payment(self, amount: float) -> bool:
...
class CreditCardProcessor:
def process_payment(self, amount: float) -> bool:
# Logika kartu kredit
return True
class PayPalProcessor:
def process_payment(self, amount: float) -> bool:
# Logika PayPal
return True
# Mudah diperluas tanpa memodifikasi kode yang ada
class CryptoProcessor:
def process_payment(self, amount: float) -> bool:
# Logika kriptocurrency
return True
Prinsip Substitusi Liskov (LSP)
Objek harus dapat diganti dengan subtipenya tanpa merusak program:
from abc import ABC, abstractmethod
class DataStore(ABC):
@abstractmethod
def save(self, key: str, value: str) -> None:
pass
@abstractmethod
def get(self, key: str) -> str:
pass
class PostgreSQLStore(DataStore):
def save(self, key: str, value: str) -> None:
# Implementasi PostgreSQL
pass
def get(self, key: str) -> str:
# Implementasi PostgreSQL
return ""
class RedisStore(DataStore):
def save(self, key: str, value: str) -> None:
# Implementasi Redis
pass
def get(self, key: str) -> str:
# Implementasi Redis
return ""
# Keduanya dapat digunakan secara bergantian
def process_data(store: DataStore, key: str, value: str):
store.save(key, value)
return store.get(key)
Prinsip Pemisahan Antarmuka (ISP)
Klien tidak boleh dipaksa bergantung pada antarmuka yang tidak mereka gunakan:
# Buruk: Antarmuka yang lebar
class Worker(ABC):
@abstractmethod
def work(self): pass
@abstractmethod
def eat(self): pass
@abstractmethod
def sleep(self): pass
# Baik: Antarmuka yang terpisah
class Workable(Protocol):
def work(self) -> None: ...
class Eatable(Protocol):
def eat(self) -> None: ...
class Human:
def work(self) -> None:
print("Bekerja")
def eat(self) -> None:
print("Makan")
class Robot:
def work(self) -> None:
print("Bekerja")
# Metode makan tidak diperlukan
Prinsip Inversi Ketergantungan (DIP)
Modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah. Keduanya harus bergantung pada abstraksi:
from typing import Protocol
# Abstraksi
class EmailSender(Protocol):
def send(self, to: str, subject: str, body: str) -> None:
...
# Modul tingkat rendah
class SMTPEmailSender:
def send(self, to: str, subject: str, body: str) -> None:
# Implementasi SMTP
pass
# Modul tingkat tinggi bergantung pada abstraksi
class UserRegistrationService:
def __init__(self, email_sender: EmailSender):
self.email_sender = email_sender
def register(self, email: str, name: str):
# Logika pendaftaran
self.email_sender.send(
to=email,
subject="Selamat Datang!",
body=f"Hello {name}"
)
Pola Repository: Mengabstraksi Akses Data
Pola Repository menyediakan antarmuka seperti koleksi untuk mengakses objek domain, menyembunyikan detail penyimpanan data.
Implementasi Repository Dasar
from abc import ABC, abstractmethod
from typing import List, Optional
from dataclasses import dataclass
from uuid import UUID, uuid4
@dataclass
class User:
id: UUID
email: str
name: str
is_active: bool = True
class UserRepository(ABC):
@abstractmethod
def save(self, user: User) -> User:
pass
@abstractmethod
def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
@abstractmethod
def get_by_email(self, email: str) -> Optional[User]:
pass
@abstractmethod
def list_all(self) -> List[User]:
pass
@abstractmethod
def delete(self, user_id: UUID) -> bool:
pass
Implementasi SQLAlchemy
from sqlalchemy import create_engine, Column, String, Boolean
from sqlalchemy.dialects.postgresql import UUID as PGUUID
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, Session
Base = declarative_base()
class UserModel(Base):
__tablename__ = 'users'
id = Column(PGUUID(as_uuid=True), primary_key=True)
email = Column(String, unique=True, nullable=False)
name = Column(String, nullable=False)
is_active = Column(Boolean, default=True)
class SQLAlchemyUserRepository(UserRepository):
def __init__(self, session: Session):
self.session = session
def save(self, user: User) -> User:
user_model = UserModel(
id=user.id,
email=user.email,
name=user.name,
is_active=user.is_active
)
self.session.add(user_model)
self.session.commit()
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.id == user_id
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def get_by_email(self, email: str) -> Optional[User]:
user_model = self.session.query(UserModel).filter(
UserModel.email == email
).first()
if not user_model:
return None
return User(
id=user_model.id,
email=user_model.email,
name=user_model.name,
is_active=user_model.is_active
)
def list_all(self) -> List[User]:
users = self.session.query(UserModel).all()
return [
User(
id=u.id,
email=u.email,
name=u.name,
is_active=u.is_active
)
for u in users
]
def delete(self, user_id: UUID) -> bool:
result = self.session.query(UserModel).filter(
UserModel.id == user_id
).delete()
self.session.commit()
return result > 0
Repository In-Memory untuk Pengujian
class InMemoryUserRepository(UserRepository):
def __init__(self):
self.users: dict[UUID, User] = {}
def save(self, user: User) -> User:
self.users[user.id] = user
return user
def get_by_id(self, user_id: UUID) -> Optional[User]:
return self.users.get(user_id)
def get_by_email(self, email: str) -> Optional[User]:
for user in self.users.values():
if user.email == email:
return user
return None
def list_all(self) -> List[User]:
return list(self.users.values())
def delete(self, user_id: UUID) -> bool:
if user_id in self.users:
del self.users[user_id]
return True
return False
Lapisan Layanan: Mengatur Logika Bisnis
Lapisan Layanan mengimplementasikan kasus penggunaan dan mengatur alur antara repositori, layanan eksternal, dan logika domain.
from typing import Optional
from uuid import uuid4
class UserAlreadyExistsError(Exception):
pass
class UserNotFoundError(Exception):
pass
class UserService:
def __init__(
self,
user_repository: UserRepository,
email_service: EmailSender,
event_publisher: 'EventPublisher'
):
self.user_repository = user_repository
self.email_service = email_service
self.event_publisher = event_publisher
def register_user(self, email: str, name: str) -> User:
# Memeriksa apakah pengguna sudah ada
existing_user = self.user_repository.get_by_email(email)
if existing_user:
raise UserAlreadyExistsError(f"Pengguna dengan email {email} sudah ada")
# Membuat pengguna baru
user = User(
id=uuid4(),
email=email,
name=name,
is_active=True
)
# Menyimpan ke repositori
user = self.user_repository.save(user)
# Mengirim email selamat datang
self.email_service.send(
to=user.email,
subject="Selamat Datang!",
body=f"Hello {user.name}, selamat datang di platform kami!"
)
# Mem-publish event
self.event_publisher.publish('user.registered', {
'user_id': str(user.id),
'email': user.email
})
return user
def deactivate_user(self, user_id: UUID) -> User:
user = self.user_repository.get_by_id(user_id)
if not user:
raise UserNotFoundError(f"Pengguna {user_id} tidak ditemukan")
user.is_active = False
user = self.user_repository.save(user)
self.event_publisher.publish('user.deactivated', {
'user_id': str(user.id)
})
return user
Injeksi Ketergantungan dalam Python
Sifat dinamis Python membuat injeksi ketergantungan menjadi sederhana tanpa memerlukan kerangka kerja berat.
Injeksi Konstruktor
class OrderService:
def __init__(
self,
order_repository: 'OrderRepository',
payment_processor: PaymentProcessor,
notification_service: 'NotificationService'
):
self.order_repository = order_repository
self.payment_processor = payment_processor
self.notification_service = notification_service
def place_order(self, order_data: dict):
# Menggunakan ketergantungan yang diinjeksikan
pass
Kontainer Ketergantungan Sederhana
from typing import Dict, Type, Callable, Any
class Container:
def __init__(self):
self._services: Dict[Type, Callable] = {}
self._singletons: Dict[Type, Any] = {}
def register(self, interface: Type, factory: Callable):
self._services[interface] = factory
def register_singleton(self, interface: Type, instance: Any):
self._singletons[interface] = instance
def resolve(self, interface: Type):
if interface in self._singletons:
return self._singletons[interface]
factory = self._services.get(interface)
if factory:
return factory(self)
raise ValueError(f"Tidak ada pendaftaran ditemukan untuk {interface}")
# Penggunaan
def create_container() -> Container:
container = Container()
# Mendaftarkan layanan
container.register_singleton(
Session,
sessionmaker(bind=create_engine('postgresql://...'))()
)
container.register(
UserRepository,
lambda c: SQLAlchemyUserRepository(c.resolve(Session))
)
container.register(
EmailSender,
lambda c: SMTPEmailSender()
)
container.register(
UserService,
lambda c: UserService(
c.resolve(UserRepository),
c.resolve(EmailSender),
c.resolve(EventPublisher)
)
)
return container
Arsitektur Hexagonal (Port dan Adapter)
Arsitektur Hexagonal menempatkan logika bisnis di tengah dengan adapter yang menangani komunikasi eksternal.
Mendefinisikan Port (Antarmuka)
# Port Input (Primair)
class CreateUserUseCase(Protocol):
def execute(self, request: 'CreateUserRequest') -> 'CreateUserResponse':
...
# Port Output (Sekunder)
class UserPersistencePort(Protocol):
def save(self, user: User) -> User:
...
def find_by_email(self, email: str) -> Optional[User]:
...
Mengimplementasikan Adapter
from pydantic import BaseModel, EmailStr
# Adapter Input (API REST)
from fastapi import FastAPI, Depends, HTTPException
class CreateUserRequest(BaseModel):
email: EmailStr
name: str
class CreateUserResponse(BaseModel):
id: str
email: str
name: str
app = FastAPI()
@app.post("/users", response_model=CreateUserResponse)
def create_user(
request: CreateUserRequest,
user_service: UserService = Depends(get_user_service)
):
try:
user = user_service.register_user(
email=request.email,
name=request.name
)
return CreateUserResponse(
id=str(user.id),
email=user.email,
name=user.name
)
except UserAlreadyExistsError as e:
raise HTTPException(status_code=400, detail=str(e))
# Adapter Output (Database)
# Sudah diimplementasikan sebagai SQLAlchemyUserRepository
Pola Desain Berbasis Domain
Objek Nilai
Objek tidak dapat diubah yang didefinisikan oleh atributnya:
from dataclasses import dataclass
from typing import Pattern
import re
@dataclass(frozen=True)
class Email:
value: str
EMAIL_PATTERN: Pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
def __post_init__(self):
if not self.EMAIL_PATTERN.match(self.value):
raise ValueError(f"Email tidak valid: {self.value}")
def __str__(self):
return self.value
@dataclass(frozen=True)
class Money:
amount: float
currency: str
def __post_init__(self):
if self.amount < 0:
raise ValueError("Jumlah tidak boleh negatif")
if self.currency not in ['USD', 'EUR', 'GBP']:
raise ValueError(f"Mata uang tidak didukung: {self.currency}")
def add(self, other: 'Money') -> 'Money':
if self.currency != other.currency:
raise ValueError("Tidak dapat menambahkan mata uang berbeda")
return Money(self.amount + other.amount, self.currency)
Agregat
Kluster objek domain yang dianggap sebagai unit tunggal:
from dataclasses import dataclass, field
from typing import List
from datetime import datetime
@dataclass
class OrderItem:
product_id: UUID
quantity: int
price: Money
def total(self) -> Money:
return Money(
self.price.amount * self.quantity,
self.price.currency
)
@dataclass
class Order:
id: UUID
customer_id: UUID
items: List[OrderItem] = field(default_factory=list)
status: str = "pending"
created_at: datetime = field(default_factory=datetime.now)
def add_item(self, product_id: UUID, quantity: int, price: Money):
item = OrderItem(product_id, quantity, price)
self.items.append(item)
def remove_item(self, product_id: UUID):
self.items = [
item for item in self.items
if item.product_id != product_id
]
def total(self) -> Money:
if not self.items:
return Money(0, "USD")
return sum(
(item.total() for item in self.items),
Money(0, self.items[0].price.currency)
)
def confirm(self):
if not self.items:
raise ValueError("Tidak dapat mengonfirmasi pesanan kosong")
if self.status != "pending":
raise ValueError("Pesanan sudah diproses")
self.status = "confirmed"
Event Domain
Event domain memungkinkan ketergantungan longgar antar komponen dan mendukung arsitektur berbasis event. Untuk sistem berbasis event skala produksi, pertimbangkan mengimplementasikan streaming event dengan layanan seperti AWS Kinesis—lihat Membangun Mikroservis Berbasis Event dengan AWS Kinesis untuk panduan lengkap.
from dataclasses import dataclass
from datetime import datetime
from typing import List, Callable
@dataclass
class DomainEvent:
occurred_at: datetime = field(default_factory=datetime.now)
@dataclass
class OrderConfirmed(DomainEvent):
order_id: UUID
customer_id: UUID
total: Money
class EventPublisher:
def __init__(self):
self._handlers: Dict[Type, List[Callable]] = {}
def subscribe(self, event_type: Type, handler: Callable):
if event_type not in self._handlers:
self._handlers[event_type] = []
self._handlers[event_type].append(handler)
def publish(self, event: DomainEvent):
event_type = type(event)
handlers = self._handlers.get(event_type, [])
for handler in handlers:
handler(event)
Fitur Python Modern untuk Arsitektur Bersih
Fitur modern Python membuat implementasi arsitektur bersih lebih elegan dan aman tipe. Jika Anda membutuhkan referensi cepat untuk sintaks dan fitur Python, lihat Python Cheatsheet.
Petunjuk Tipe dan Protokol
from typing import Protocol, runtime_checkable
@runtime_checkable
class Serializable(Protocol):
def to_dict(self) -> dict:
...
@classmethod
def from_dict(cls, data: dict) -> 'Serializable':
...
def serialize(obj: Serializable) -> dict:
return obj.to_dict()
Pydantic untuk Validasi
from pydantic import BaseModel, Field, validator
from typing import Optional
class CreateUserDTO(BaseModel):
email: EmailStr
name: str = Field(..., min_length=2, max_length=100)
age: Optional[int] = Field(None, ge=0, le=150)
@validator('name')
def name_must_not_contain_numbers(cls, v):
if any(char.isdigit() for char in v):
raise ValueError('Nama tidak boleh mengandung angka')
return v
class Config:
frozen = True # Membuat objek tidak dapat diubah
Async/Await untuk Operasi I/O
Sintaks async/await Python sangat kuat untuk operasi I/O-bound dalam arsitektur bersih, memungkinkan interaksi non-blocking dengan database dan layanan eksternal. Saat mendeploy aplikasi Python ke platform serverless, memahami karakteristik kinerja menjadi penting—lihat AWS lambda performance: JavaScript vs Python vs Golang untuk wawasan mengenai optimisasi fungsi serverless Python.
from typing import List
import asyncio
class AsyncUserRepository(ABC):
@abstractmethod
async def save(self, user: User) -> User:
pass
@abstractmethod
async def get_by_id(self, user_id: UUID) -> Optional[User]:
pass
class AsyncUserService:
def __init__(self, repository: AsyncUserRepository):
self.repository = repository
async def register_user(self, email: str, name: str) -> User:
user = User(id=uuid4(), email=email, name=name)
return await self.repository.save(user)
async def get_users_batch(self, user_ids: List[UUID]) -> List[User]:
tasks = [self.repository.get_by_id(uid) for uid in user_ids]
results = await asyncio.gather(*tasks)
return [u for u in results if u is not None]
Praktik Terbaik Struktur Proyek
Organisasi proyek yang tepat sangat penting untuk mempertahankan arsitektur bersih. Sebelum mengatur struktur proyek Anda, pastikan Anda menggunakan lingkungan virtual Python untuk isolasi dependensi. venv Cheatsheet mencakup segala sesuatu yang perlu Anda ketahui tentang mengelola lingkungan virtual. Untuk proyek Python modern, pertimbangkan uv - New Python Package, Project, and Environment Manager, yang menyediakan manajemen paket dan pengaturan proyek yang lebih cepat.
my_application/
├── domain/ # Aturan bisnis perusahaan
│ ├── __init__.py
│ ├── entities/
│ │ ├── __init__.py
│ │ ├── user.py
│ │ └── order.py
│ ├── value_objects/
│ │ ├── __init__.py
│ │ ├── email.py
│ │ └── money.py
│ ├── events/
│ │ ├── __init__.py
│ │ └── user_events.py
│ └── exceptions.py
│
├── application/ # Aturan bisnis aplikasi
│ ├── __init__.py
│ ├── use_cases/
│ │ ├── __init__.py
│ │ ├── create_user.py
│ │ └── place_order.py
│ ├── services/
│ │ ├── __init__.py
│ │ └── user_service.py
│ └── ports/
│ ├── __init__.py
│ ├── repositories.py
│ └── external_services.py
│
├── infrastructure/ # Antarmuka eksternal
│ ├── __init__.py
│ ├── persistence/
│ │ ├── __init__.py
│ │ ├── sqlalchemy/
│ │ │ ├── models.py
│ │ │ └── repositories.py
│ │ └── mongodb/
│ │ └── repositories.py
│ ├── messaging/
│ │ ├── __init__.py
│ │ └── rabbitmq_publisher.py
│ ├── external_services/
│ │ ├── __init__.py
│ │ └── email_service.py
│ └── config.py
│
├── presentation/ # Lapisan UI/API
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── dependencies.py
│ │ ├── routes/
│ │ │ ├── __init__.py
│ │ │ ├── users.py
│ │ │ └── orders.py
│ │ └── schemas/
│ │ ├── __init__.py
│ │ └── user_schemas.py
│ └── cli/
│ └── commands.py
│
├── tests/
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── main.py # Titik masuk aplikasi
├── container.py # Pengaturan injeksi ketergantungan
├── pyproject.toml
└── README.md
Pengujian Arsitektur Bersih
Pengujian Unit Logika Domain
import pytest
from uuid import uuid4
def test_user_creation():
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
assert user.email == "test@example.com"
assert user.is_active is True
def test_order_total_calculation():
order = Order(id=uuid4(), customer_id=uuid4())
order.add_item(
uuid4(),
quantity=2,
price=Money(10.0, "USD")
)
order.add_item(
uuid4(),
quantity=1,
price=Money(5.0, "USD")
)
assert order.total().amount == 25.0
Pengujian Integrasi dengan Repository
@pytest.fixture
def in_memory_repository():
return InMemoryUserRepository()
def test_user_repository_save_and_retrieve(in_memory_repository):
user = User(
id=uuid4(),
email="test@example.com",
name="Test User"
)
saved_user = in_memory_repository.save(user)
retrieved_user = in_memory_repository.get_by_id(user.id)
assert retrieved_user is not None
assert retrieved_user.email == user.email
Pengujian Lapisan Layanan
from unittest.mock import Mock
def test_user_registration():
# Menyiapkan
mock_repository = Mock(spec=UserRepository)
mock_repository.get_by_email.return_value = None
mock_repository.save.return_value = User(
id=uuid4(),
email="test@example.com",
name="Test"
)
mock_email = Mock(spec=EmailSender)
mock_events = Mock(spec=EventPublisher)
service = UserService(mock_repository, mock_email, mock_events)
# Melakukan
user = service.register_user("test@example.com", "Test")
# Memverifikasi
assert user.email == "test@example.com"
mock_repository.save.assert_called_once()
mock_email.send.assert_called_once()
mock_events.publish.assert_called_once()
Kesalahan Umum dan Cara Menghindarinya
Terlalu Rumit
Jangan implementasikan arsitektur bersih untuk aplikasi CRUD sederhana. Mulailah dengan sederhana dan refactor saat kompleksitas meningkat.
Abstraksi Bocor
Pastikan entitas domain tidak mengandung anotasi database atau kode spesifik framework:
# Buruk
from sqlalchemy import Column
@dataclass
class User:
id: Column(Integer, primary_key=True) # Framework bocor ke domain
# Baik
@dataclass
class User:
id: UUID # Objek domain murni
Ketergantungan Sirkular
Gunakan injeksi ketergantungan dan antarmuka untuk memecah ketergantungan sirkular antar lapisan.
Mengabaikan Konteks
Arsitektur bersih bukan satu ukuran untuk semua. Sesuaikan ketat lapisan berdasarkan ukuran proyek dan keahlian tim.
Tautan Berguna
- Clean Architecture oleh Robert C. Martin
- Dokumentasi Python Type Hints
- Dokumentasi Pydantic
- Dokumentasi Resmi FastAPI
- Dokumentasi ORM SQLAlchemy
- Perpustakaan Dependency Injector
- Referensi Domain-Driven Design
- Arsitektur Pola dengan Python
- Blog Martin Fowler tentang Arsitektur
- Panduan Pola Desain Python
- Python Cheatsheet
- venv Cheatsheet
- uv - New Python Package, Project, and Environment Manager
- AWS lambda performance: JavaScript vs Python vs Golang
- Membangun Microservices Berbasis Event dengan AWS Kinesis