past architektury
ERP a CRM systémy přitahují architektonické cosplay rychleji než skoro jakákoli jiná kategorie business softwaru. Částečně proto, že ten problém vypadá obrovsky už první den. Hned si představíte sklad, fakturaci, procurement, approval flow, zákaznické záznamy, email sync, operace ve skladu, všechno namačkané v jedné codebase. Tahle mentální představa pak lidi tlačí k service boundaries dávno předtím, než mají jediný důkaz, že ty hranice jsou skutečné.
Viděl jsem týmy s dvanácti zákazníky, které si rozsekaly systém na samostatné služby pro auth, billing, notifications, reporting, dokumenty a workflow automation. Následujících šest měsíců pak strávily psaním glue code, aby se jejich vlastní systém vůbec dal používat. Měly RabbitMQ, Redis, OpenTelemetry, service mesh, kterému sotva rozuměly, a Terraform moduly rozdělené do šesti repozitářů. Přesto nedokázaly odpovědět ani na základní support dotaz bez grepování logů přes půl stacku. U jednoho týmu padal noční reconciliation job, protože jedna služba publikovala customer.updated s account_id jako string, druhá ho četla jako integer a jediný viditelný symptom byl, že dashboard každé ráno ujížděl o 37 záznamů.
Zní to sofistikovaně jen do chvíle, než máte on-call.
Startupové ERP má obvykle jeden skutečný úkol: rychle zakódovat chaotická business pravidla, průběžně je měnit a celou dobu držet data konzistentní. Django 4.2 monolit nad PostgreSQL 15 je na to brutálně dobrý. Dostanete transakce, které opravdu pokrývají celou business operaci, admin nástroje, které support tým použije příští týden, migrace, nad kterými se dá normálně přemýšlet, jeden deployment artifact, jednu observability plochu, jedno místo pro testy a jedno místo, kam dát breakpoint. To není nostalgie. To je provozní příčetnost.
Ve Steezru stavíme hodně interních systémů pro SMB a startupy, ERP-ish platformy, zákaznické portály, sales automation, document workflow. Ten pattern se opakuje pořád dokola. Týmy, které zůstanou déle u monolitu, shipují rychleji, debugují rychleji a neutrácejí engineering budget za daň z distribuovaných systémů, ale za doménovou logiku.
proč django do ERP sedí
Django 4.2 má přesně ten typ nudné síly, který ERP software potřebuje. ORM je vyzrálé, admin je pořád absurdně podceňovaný, request lifecycle je čitelný a ekosystém kolem formulářů, oprávnění, storage, background jobů a testování je dost hluboký na to, abyste skoro nikdy nemuseli být za každou cenu chytří. Chytrost je drahá.
PostgreSQL 15 odvede ještě víc těžké práce, než si většina týmů připouští. Row locking přes select_for_update, partial indexes, materialized views pro ošklivé reporting query, JSONB na občasný nehezký vendor payload, pg_trgm pro fuzzy search nad názvy zákazníků, partitioning pro event tabulky, pokud to opravdu potřebujete. To všechno v jedné databázi, do které se tým podívá přes psql a rozumí jí. Jakmile přestanete předstírat, že vaše data chtějí být rozsekaná do pěti databází, obrovské množství business problémů se dramaticky zjednoduší.
Dobře sem sedí i multi-tenancy přes schémata. Jedna databáze, jeden cluster, oddělená schémata pro jednotlivé tenanty, sdílený app code. Vyhnete se nejhorším variantám bugů kolem row-based scopu, kdy někdo v jednom query zapomene tenant_id a najednou Acme Corp vidí faktury Globexu. Zároveň se vyhnete provoznímu bordelu, kdy příliš brzy zakládáte samostatnou databázi pro každého malého zákazníka. Balíčky jako django-tenants jsou použitelné, pokud si v tom udržíte jasno a nezačnete bojovat s abstrakcí.
Normální setup může vypadat třeba takhle:
1# settings.py2INSTALLED_APPS = [3 "django_tenants",4 "customers",5 "erp.core",6 "erp.sales",7 "erp.inventory",8 "erp.billing",9]1011DATABASES = {12 "default": {13 "ENGINE": "django_tenants.postgresql_backend",14 "NAME": "erp",15 "USER": "erp",16 "PASSWORD": os.environ["DB_PASSWORD"],17 "HOST": "db",18 "PORT": "5432",19 }20}2122TENANT_MODEL = "customers.Client"23TENANT_DOMAIN_MODEL = "customers.Domain"
Na async práci pak použijte Celery s Redisem nebo RabbitMQ, pokud opravdu potřebujete broker semantics. Generování PDF, EDI importy, retry webhooků, OCR pipeline, posílání emailů, to všechno jsou perfektní async kandidáti. Chyba je udělat z každé doménové interakce async choreografii. create_invoice() má většinou fakturu zapsat ve stejné transakci, ne vyemitovat tři eventy a doufat, že eventual consistency to nějak zachrání.
distribuovaná bolest je reálná
Prodejní argument pro microservices se vždy točí kolem nezávislého škálování a autonomie týmů. Ve čtyřicetičlenné platform organizaci to zní skvěle. V šestičlenném startupu už o dost míň, zvlášť když produkční stack umí pod tlakem opravit pořádně jen dva lidi. Každá další služba znamená další CI pipeline, další deploy, další sadu secrets, další migrační cestu, další alert channel a další způsob, jak se to rozbije jen při částečném výpadku.
Plnou cenu nevidíte ve fázi architektonického diagramu. Uvidíte ji ve chvíli, kdy billing service timeoutuje při čekání na customer service, ta je degraded, protože jí došel connection pool, ten došel proto, že reporting job fan-outem rozbil databázi, a váš endpoint pro finalizaci faktur teď vrací 502 Bad Gateway, ale jen některým tenantům. V Nginx logách jsou upstream failures, Sentry má jen útržky trace, Grafana hlásí skok v p95 a vaši inženýři ručně párují timestampy přes tři repozitáře, protože jeden interní klient neposílal trace context header.
Message brokery přidávají vlastní kategorii nesmyslů. Duplicitní doručení, poison messages, zastaralí consumeři, schema drift, dead-letter queue, které se potichu plní celé dny, protože nikoho nenapadlo přidat alert. Týmy, které sotva zvládají udržet čisté synchronní code path, se najednou dobrovolně pouštějí do eventual consistency a exactly-once semantics. Ani jedno neexistuje tak jednoduše, jak si myslí. Pak přijdou compensating actions, idempotency keys, retry s exponential backoff a nevyhnutelný postmortem, kde někdo pronese, že nám architektura dala flexibilitu.
Flexibilitu na co přesně?
ERP je plné provázaných operací. Schválit purchase order, rezervovat sklad, vytvořit payable, aktualizovat ledger, někoho notifikovat, možná vygenerovat PDF. Tyhle akce k sobě logicky patří. Pokud uživatel čeká jeden konzistentní výsledek, měl by systém nejspíš provést hlavní změny stavu uvnitř jedné databázové transakce. Když to rozdělíte příliš brzy, koupíte si teoretickou eleganci a velmi reálné chyby.
Monolit samozřejmě taky selhává. Rozdíl je v tom, že většina selhání je viditelná, lokální a opravitelná bez otevírání sedmi dashboardů.
postavte monolit správně
Monolit neznamená odkládací šuplík na všechno. Codebase musí držet hranice i uvnitř jednoho deployovatelného celku, jinak skončíte ve stejném chaosu jako u microservices, jen bez síťové latence. Organizujte kód podle domén, držte rozhraní explicitní a zakažte náhodné importy mezi moduly, pokud směr závislosti není záměrný.
Struktura složek, která se nám osvědčila, vypadá zhruba takhle:
1erp/2 sales/3 services/4 models.py5 selectors.py6 api.py7 inventory/8 services/9 models.py10 selectors.py11 api.py12 billing/13 services/14 models.py15 selectors.py16 api.py17 shared/18 money.py19 events.py
services/ drží business akce, které mění stav, selectors.py řeší read query a api.py je tenká hranice, přes kterou volají ostatní moduly. Žádný modul nesahá přímo do modelů jiného modulu, pokud jste tu závislost výslovně neposvětili. Tohle jediné pravidlo vám ušetří spoustu bolesti, až někdy budete něco vysekávat ven.
Používejte Postgres constraints agresivně. Unique constraints, check constraints, foreign keys, podle potřeby i exclusion constraints. Jestli na pravidlech rezervace skladu záleží, zakódujte je co nejvíc do databáze. Aplikační kód pod zátěží lže. Databáze s tím mívá menší trpělivost.
Kolem business operací, které musí zůstat konzistentní, dávejte transaction.atomic():
1from django.db import transaction23@transaction.atomic4def approve_purchase_order(po_id: int, actor_id: int) -> None:5 po = (6 PurchaseOrder.objects7 .select_for_update()8 .get(id=po_id)9 )10 if po.status != PurchaseOrder.Status.SUBMITTED:11 raise InvalidState("PO must be submitted before approval")1213 po.status = PurchaseOrder.Status.APPROVED14 po.approved_by_id = actor_id15 po.save(update_fields=["status", "approved_by_id"])1617 LedgerEntry.objects.create(...)18 NotificationOutbox.objects.create(...)
Všimněte si outbox tabulky. To je jeden z mála patternů, které doporučuju nasadit brzy, protože čistě propojí synchronní změny stavu a async side effecty. Business data i outbound eventy commitnete v jedné transakci a Celery worker pak později publikuje emaily, webhooky nebo zprávy dál po systému. Žádný dual-write bordel, žádné ztracené notifikace, když worker spadne ve špatný moment.
Tahle architektura škáluje překvapivě daleko. Obvykle dost daleko na to, aby váš další problém byl product-market fit, ne service decomposition.
rozdělujte až když to bolí
Služby byste měli vysekávat ven proto, že vás opakovaně bolí konkrétní bottleneck, ne proto, že někdo četl o bounded contexts a nadchnul se. Chci k tomu napsaný důvod a čísla. Saturace CPU v jednom modulu. Reálný konflikt v deployment cadence mezi týmy. Compliance požadavek, který vynucuje izolaci. Datový model, který se rozešel natolik, že sdílené transakce jsou teď větší problém. Cokoli konkrétního. Všechno méně konkrétní je architektonická fan fiction.
Nejbezpečnější cesta začíná uvnitř monolitu, se stabilním rozhraním a bez sítě. Nejdřív si definujte adapter boundary:
1# billing/api.py2class BillingGateway(Protocol):3 def create_invoice(self, order_id: int) -> str: ...45class LocalBillingGateway:6 def create_invoice(self, order_id: int) -> str:7 return create_invoice(order_id)
Pak to zapojte přes settings nebo dependency injection a za feature flag přidejte remote implementaci.
1# settings.py2BILLING_BACKEND = os.getenv("BILLING_BACKEND", "local")
1class RemoteBillingGateway:2 def create_invoice(self, order_id: int) -> str:3 r = httpx.post(4 "http://billing:8000/api/invoices/",5 json={"order_id": order_id},6 timeout=5.0,7 )8 r.raise_for_status()9 return r.json()["invoice_id"]
Teď můžete shadowovat traffic, zapnout to pro jednoho tenanta, porovnat výstupy a udělat rollback bez přepisování zbytku aplikace. Takhle vypadá dospělé inženýrství.
U dat většinou preferuju database-per-service ve chvíli, kdy je služba opravdu vytažená ven, protože sdílená databáze drží skryté coupling při životě navždy. Během migrace kopírujte data přes CDC nebo plánovaný sync, nechte jednu stranu jako autoritativní zdroj a vlastnictví popište naprosto jednoznačně v dokumentaci i v code review. Tady se hodí i feature flags. django-waffle na rollout po tenantech úplně stačí.
Rozumná sekvence vypadá takto: nejdřív izolovat code path uvnitř monolitu, potom přidat API kontrakty a contract testy, pak mirrorovat write nebo read pro úzký výsek, následně přepnout jednoho tenanta nebo jeden workflow a teprve potom měřit support load a provozní šum, než to rozšíříte dál. Jestli nová služba generuje víc pager hluku než business hodnoty, zrušte ji. Možnost vrátit se zpátky je důležitější než architektonická čistota.
rozumný výchozí stack
Kdybych příští pondělí rozjížděl ERP nebo CRM platformu pro startup, který fakt musí dodávat, vzal bych Django 4.2 LTS, PostgreSQL 15, Celery 5.3, Redis 7 na cache a queueing, pokud zrovna opravdu nepotřebujete broker guarantees, HTMX pro interní backoffice interakce, které si plnohodnotné SPA nezaslouží, a Next.js 14 jen tam, kde z toho customer-facing část opravdu těží. Spousta admin-heavy business softwaru se zhorší ve chvíli, kdy se z každé obrazovky stane frontend architektonické cvičení.
I deployment může zůstat nudný. Jeden app container, jeden worker container, jeden beat scheduler, pokud potřebujete periodické joby, před tím Nginx nebo Caddy, managed Postgres, pokud to rozpočet dovolí. Hoďte to na Fly.io, Render, Railway, ECS nebo malý Kubernetes cluster, pokud váš tým Kubernetes už dobře zná. To je celkem jedno. Důležité je, aby každý v týmu dokázal vystopovat request z browseru až do databáze bez toho, aby si nejdřív musel číst platform runbook.
Nudný by měl zůstat i monitoring. Sentry na chyby, Prometheus plus Grafana, pokud potřebujete hlubší metriky, strukturované JSON logy poslané do Loki nebo do čehokoli, co už tým používá. Do non-prod přidejte django-silk nebo django-debug-toolbar, pomalé query kontrolujte přes pg_stat_statements a nejdřív opravte očividné věci, než někdo začne tvrdit, že monolit neškáluje. V půlce případů je bottleneck N+1 query schované v serializeru, ne architektonický styl.
Viděl jsem příliš mnoho týmů, které měsíce inženýrsky obcházely problémy, které ještě ani neměly, zatímco reálná bolest zákazníků ležela netknutá bokem. Nejlepší ERP architektura pro early-stage firmu je ta, která seniorním inženýrům umožní rychle shipovat účetní pravidla, approval chainy, importy, role permissions a ošklivé integrace, a přitom udrží produkci srozumitelnou. To je monolit.
Složitost si zaslužte až později.
