Introduction
What if 80% of your microservices should actually be a modular monolith?
Over the weekend, a particular train of thought captured my attention—patterns in system behavior and design. As we transition from mainframes to modern Java architectures and tackle modernization initiatives, a question keeps surfacing: How much are we just reinventing the wheel?
This piece dives deep into replacement, focusing on modular monoliths, centralized transactions, and RACF-style authorization. What follows are exploratory notes—not definitive opinions, but a structured examination that continues to evolve.
Modern cloud-native frameworks optimize for different goals: developer velocity, containerization, and horizontal scaling. But in doing so, have we sacrificed something important? The overarching lesson seems to be that modern systems often layer complexity to compensate for flexibility, whereas the mainframe principles of discipline, cohesion, and predictable transactions remain highly effective when consciously applied.
So the question isn’t whether we can modernize mainframe applications. The question is: Will we have the discipline to do it right?
The Modern Stack for Mainframe Replacement
When replacing a COBOL/CICS/DB2 mainframe application, the guiding principles are usually:
- Correctness over cleverness
- Strong transactional boundaries
- Explicit domain modeling
- Incremental migration
- Boring tech wins
Technology Stack
- Runtime: Quarkus (JVM mode initially)
- Architecture: Modular Monolith
- Domain: Clean Architecture / DDD
- Persistence: JPA + RDBMS (PostgreSQL/Oracle/DB2)
- Transactions: JTA (Narayanas)
- APIs: JAX-RS
- Messaging: Kafka
- Security: OIDC / OAuth2
- Deployment: Containers or VMs
Why Modular Monolith First?
Not microservices on day one. Here’s why:
- One deployable unit with strict module boundaries
- Internal APIs enforced at compile time
- Horizontal scaling comes later, after correctness is proven
- Mainframes are monoliths for good reason—cohesion matters
The “Centralized Hybrid” Pattern
A team initially tried a “microservices-first” approach. The result was a proliferation of network latency and “distributed transaction” nightmares that the mainframe simply never had. The solution? Pivot to a Modular Monolith running on Quarkus.
By centralizing the transaction manager (JTA) and strictly enforcing domain boundaries within a single deployable unit, they achieved:
- 30% reduction in operational complexity.
- Sub-millisecond latency for intra-module calls (replacing REST/gRPC).
- Consolidated logging and auditing that mirrored the reliability of the original mainframe environment.
The takeaway was clear: Scale the region, not the individual program, until you have a genuine organizational reason to split.
Replacing CICS and DB2
What CICS Actually Provided
People often underestimate CICS. It wasn’t just “an app server”:
- Ultra-low-latency OLTP
- Millions of short-lived transactions
- Strong ACID semantics
- Terminal/session handling
- Integrated security and resource management
- Extreme reliability
Modern CICS Replacement
The replacement isn’t one thing—it’s a stack:
- CICS program: Java service / use-case class
- CICS transaction: REST endpoint or message handler
- COMMAREA: Request DTO / command object
- Syncpoint: JTA transaction
- TSQ / TDQ: Database table / Kafka topic
- BMS screen: Web UI / API consumer
- CICS region: App instance / pod
- CICS security: IAM + OAuth + RBAC
Key insight: CICS transactions map cleanly to short-lived stateless service calls. The challenge is recreating CICS’s predictability, not its APIs.
Database Workload Replacement
For online (OLTP) database access:
- Stick with relational databases (PostgreSQL, Oracle, DB2 LUW)
- Use JPA (Hibernate) or plain SQL for migrated logic
- Do not get creative with NoSQL for core state
For batch database workloads:
- Spring Batch (still best in class)
- Explicit transactions with checkpointing and restartability
- Scheduling via Control-M, Autosys, or carefully managed Kubernetes CronJobs
- Batch is not microservices—treat it like batch
The Paradox: Why Are Replacements So Complex?
If CICS was so compact and effective, why do modern replacements require so many components?
CICS Was Vertically Integrated
CICS provided a single integrated universe:
- Runtime, scheduler, transaction manager, lock manager
- Security model, I/O subsystem, UI protocol (3270)
- Operations tooling, recovery system
- All designed together for one OS, one hardware model, one trust boundary
Modern systems deliberately don’t do this. We chose flexibility over cohesion.
Locality vs. Distribution
CICS assumptions:
- Everything is local
- Memory is shared
- Latency is microseconds
- Failure is rare and restart is controlled
Modern assumptions:
- Network calls are normal
- Processes die frequently
- Machines are ephemeral
- Horizontal scaling is mandatory
- Failure is expected
You can’t have a single “compact” component when the world is not a single box anymore.
Hidden Complexity vs. Explicit Complexity
CICS handles locking, deadlock detection, recovery logging, and rollback implicitly. Modern replacements:
- Make these concerns explicit
- Expose them as separate components
- Force you to reason about them
Less magic, more knobs. Neither approach is inherently wrong—they optimize for different goals.
Cloud Services: Mainframe Concepts in Disguise
If you squint, the cloud is basically IBM circa 1978 with better UX:
- AWS Lambda / Azure Functions: CICS transactions
- RDS, Cloud SQL, Aurora: DB2 / VSAM datasets
- SQS, Pub/Sub, Service Bus: TSQ / TDQ
- Step Functions, Durable Functions: JCL workflows
- IAM, Azure AD: RACF
- CloudWatch, Stackdriver: SMF / RMF
- Auto-scaling groups: Workload Manager
The punchline: Cloud re-bundled mainframe ideas and is selling them per hour.
Building a Modular Monolith That Scales Like CICS
Core Structure
app/
├── core-domain/
│ ├── account/
│ ├── ledger/
│ ├── customer/
├── application/
│ ├── usecases/
│ ├── transactions/
├── infrastructure/
│ ├── persistence/
│ ├── messaging/
│ ├── security/
├── interfaces/
│ ├── rest/
│ ├── batch/
│ ├── messaging/
Rules:
- Core domain has zero framework dependencies
- Modules communicate via interfaces only
- No shared mutable state across modules
- One transaction manager (JTA)
- Explicit boundaries everywhere
CICS-Style Transactions
@Transactional
public void postPayment(PaymentCommand cmd) {
validate(cmd);
debitAccount();
creditAccount();
writeLedger();
}
No retries inside. No async in the middle. No cleverness. Just like CICS.
Concurrency Model
CICS’s secret sauce:
- Short transactions with strict limits
- Back-pressure and early rejection
- Bounded thread pools and queue depth limits
- Timeouts everywhere
Reject work early. Mainframes did this ruthlessly—we should too.
Scaling Strategy
Vertical first:
- Optimize single-instance throughput
- Tune GC and avoid chatty I/O
Horizontal second:
- Clone the whole monolith
- Stateless instances
- Sticky sessions if absolutely necessary
Scale the region, not the program.
Authorization: RACF-Style for Modern Systems
One critical capability from mainframes that deserves special attention is RACF-style authorization. RACF wasn’t just “login + roles”—it was a centralized policy engine that decided whether a subject could perform an action on a named resource, consistently and auditably, outside application code.
Modern applications can achieve the same discipline using tools like Keycloak:
public class AccountService {
private final AuthorizationService auth;
public void withdrawMoney(Subject subject, String accountId, BigDecimal amount) {
// Authorization at use-case boundary
auth.check(subject, Resource.account(accountId), Action.UPDATE);
// Business logic follows
// ...
}
}
Core principles:
- Centralized enforcement - One authorization service, not scattered checks
- Resource-based permissions -
ACCOUNT:12345, not just role names - Data-driven policies - Stored in Keycloak, not hardcoded in annotations
- Default deny - Nothing allowed unless explicitly permitted
- Full audit trail - Every decision logged for compliance
This approach gives you the maintainability and auditability of RACF with the flexibility of modern IAM systems. See my other post for more details: Implementing RACF-Style Authorization in Modern Java Applications**
What Modern Complexity Is Actually Unnecessary
Let’s be brutally honest about which modern patterns add more cost than value:
1. Microservices for Intra-Domain Logic
The claim: Independent deployment, team autonomy, scalability!
The reality:
- Network calls replace method calls
- Transactions get hand-waved away
- Data consistency becomes “eventual”
- Debugging becomes archaeology
Honest take: 80% of microservices should be modules in a monolith. Use services only at true organizational boundaries, not technical ones.
2. Event-Driven Everything
Events aren’t free—they’re delayed complexity.
Good use cases: Integration, auditing, notifications, long-running workflows
Cargo cult use cases: CRUD updates, core state transitions, financial postings
Honest take: If you need a dashboard to understand state, you’re doing it wrong.
3. Kubernetes as Default Runtime
K8s is powerful, but it’s not neutral. It adds 10+ failure modes, indirection everywhere, operational tax, and YAML chaos.
CICS gave you one control plane, one scheduler, one failure model.
Honest take: If you don’t need autoscaling every week, Kubernetes is probably overkill. Most enterprise workloads are boring and stable.
4. Service Meshes
Service meshes exist because we built systems that don’t trust themselves.
Instead of designing clear boundaries, enforcing contracts, and being explicit, we added sidecars, proxies, policies, and distributed config.
Honest take: A service mesh is often a tax you pay for not having a platform. CICS didn’t need one because it was the platform.
5. Reactive Programming for Business Logic
Reactive is great for UI, streaming, and backpressure-heavy I/O. It’s terrible for transactions, money, and deterministic workflows.
Honest take: If your business logic can’t be stepped through linearly, you’ve already lost. CICS was synchronous for a reason.
What Complexity Is Justified
Let’s be fair—some complexity is unavoidable:
Modern Security Threats
Modern threat models are real. Mainframes lived inside trusted perimeters. IAM, OAuth, and zero-trust architectures are necessary.
Network Unreliability
CICS lived in a fantasy world of stable networks and local resources. We don’t get that luxury anymore. Retries, idempotency, and timeouts are sadly real.
Vendor Independence
CICS was vendor lock-in by design. Modern ecosystems pay complexity costs to avoid that. It’s a conscious trade-off.
Lessons Learned
If this were running on CICS, how many of these components would actually exist?
If the answer isn’t “far fewer,” something’s wrong.
What to Re-Centralize
If I were ruthless, I’d centralize:
- Transactions — one transaction manager, one logging strategy
- Scheduling — both online and batch
- Security — central auth + authorization
- Deployment model — fewer shapes, fewer knobs
- State — one authoritative source per domain
This alone removes half the moving parts.
The Brutal Truth
Modern systems are complex because:
- We allow undisciplined architecture
- We mistake flexibility for progress
- We optimize for churn, not longevity
CICS enforced discipline by removing choice. We could do the same today—we just choose not to.
-
Modern complexity is often unnecessary:
- Microservices inside a single domain
- Event-driven logic for CRUD
- Kubernetes, service meshes, reactive programming for transactional code
-
Real benefits can come from:
- Centralized transaction management
- Modular monolith discipline
- Explicit, auditable authorization
- Stable resource naming and policy enforcement
-
Mainframe principles of discipline, cohesion, and predictable transactions still apply and reduce operational and security complexity
Conclusion
The cloud didn’t kill mainframes—it recreated them without admitting it.
CICS worked because it:
- Centralized what mattered
- Distributed only when necessary
- Enforced discipline by design
You can absolutely build that today in Java—if you’re willing to say no to 70% of modern “best practices.”
Modern Java can replace mainframes using modular monoliths, centralized transactions, and RACF-style authorization, leveraging frameworks like Quarkus and Keycloak. The key is recognizing that mainframe principles—compact, integrated, auditable, predictable systems—are still highly relevant.
The question isn’t whether we can modernize mainframe applications. The question is: Will we have the discipline to do it right?
These are exploratory notes from a weekend deep-dive into architecture patterns. This thinking will continue to evolve.
What patterns have you noticed in your modernization journeys? Share in the comments below!
If these architectural deep-dives help you, follow me for insights on enterprise architecture.
#MainframeModernization #SoftwareArchitecture #CloudNative #EnterpriseJava #TechLeadership