EG
Resume

PAMAFix SAP project hit different. 💀

System Architecture — PAMAFix Monolith to Microservices
RESTRouteRouteRouteBridgeQueryQueryQueryReact SPACOREAPI GatewayCOREOTC ServiceSERVICEOpex ServiceSERVICECapex ServiceSERVICE.NET LegacySERVICEPostgreSQLDATABASE
CORESERVICEDATABASEDATA FLOW

The Challenge

PAMA Persada had a monolithic ERP system that:

  • Was slow as hell (startup time: coffee break ☕)
  • One bug could crash everything
  • Teams stepping on each other
  • Deployments = pray no bugs 🙏

They decided: let's do microservices. so that's what we built.

What is OTC?

OTC = Order to Cash (sap jargon) it's the process from:

  1. Supplier delivers goods
  2. Create Berita Acara (delivery receipt)
  3. Invoice generation
  4. Payment tracking
  5. Reporting

simple right? except in mining industry where you have:

  • Multiple suppliers
  • Different pricing tiers
  • Dynamic taxes
  • Multiple currencies
  • Audit requirements
  • Integration with SAP (their main system)

yeah. not so simple. 😅

Tech Stack

Frontend:

  • React with TypeScript (safety first)
  • Ant Design (complex tables made somewhat manageable)
  • Zod for validation (runtime type checking)
  • React Query for server state

Backend:

  • .NET microservices (not my choice but hey, enterprise loves .NET)
  • Each domain = separate service
  • API Gateway for routing
  • PostgreSQL per service

Key Features I Built

1. Dynamic Form Handler

This was the hardest part. forms that change based on:

  • User role
  • Transaction type
  • Supplier category
  • Amount thresholds
interface FormConfig {
  fields: FieldConfig[];
  rules: ConditionalRule[];
  validation: ZodSchema;
}

const DynamicForm: React.FC<{ config: FormConfig }> = ({ config }) => {
  const { watch } = useFormContext();

  return (
    <>
      {config.fields.map(field => {
        if (isConditional(field, watch())) {
          return <DynamicField key={field.name} {...field} />;
        }
        return null;
      })}
    </>
  );
};

2. Complex Table Component

Opex reports with 18+ columns. users can:

  • Show/hide columns
  • Reorder columns
  • Pin columns
  • Export to Excel
  • Drill down into sub-items
// expandable row example
const ExpandableRow = ({ record }: { record: OpexRecord }) => {
  const [expanded, setExpanded] = useState(false);

  return (
    <>
      <tr onClick={() => setExpanded(!expanded)}>
        <td colSpan={18}>{record.summary}</td>
      </tr>
      {expanded && (
        <tr>
          <td colSpan={18}>
            <DrillDownTable data={record.details} />
          </td>
        </tr>
      )}
    </>
  );
};

3. Error Handling in Microservices

When you call 5 services to complete one operation, errors can happen anywhere:

async function createTransaction(data: TransactionInput) {
  try {
    // Call services in parallel where possible
    const [supplier, pricing, tax] = await Promise.all([
      supplierService.validate(data.supplierId),
      pricingService.calculate(data.items),
      taxService.getRates(data.category)
    ]);

    const transaction = await transactionService.create({
      ...data,
      supplier,
      pricing,
      tax
    });

    return transaction;
  } catch (error) {
    // Handle partial failures
    if (error instanceof PartialFailure) {
      // Rollback what we can
      await transactionService.rollback(error.failedOps);
      throw new UserFriendlyError(
        `Some items failed: ${error.reasons.join(', ')}`
      );
    }
    throw error;
  }
}

4. Column Management

Users wanted control over their views:

const useColumnVisibility = (defaultColumns: Column[]) => {
  const [visibleColumns, setVisibleColumns] = useState(
    () => localStorage.getItem('opex_columns') ?? defaultColumns
  );

  const toggleColumn = (colId: string) => {
    setVisibleColumns(prev =>
      prev.includes(colId)
        ? prev.filter(id => id !== colId)
        : [...prev, colId]
    );
    localStorage.setItem('opex_columns', JSON.stringify(visibleColumns));
  };

  return { visibleColumns, toggleColumn };
};

Collaboration with Backend

This was my first time working closely with .NET team. learned:

  1. API contracts matter more than ever - we used OpenAPI specs
  2. Error responses need standardization - everyone used different formats initially 💀
  3. Data consistency - eventual consistency in microservices is real
  4. Debugging distributed systems - correlation IDs saved us

Challenges

Dynamic forms are HARD every business rule seemed to add a new condition. eventually we built a rules engine. learned that building tools > building features.

Testing microservices how do you test something that depends on 5 other services? answer: test containers, mocks, and lots of patience.

Performance initial load was slow because we were fetching from multiple services. solved with:

  • React Query caching
  • Service-level caching
  • Optimistic updates

Results

  • ✅ Users can handle complex financial data entry efficiently
  • ✅ 18+ column tables with full user control
  • ✅ Resilient error handling
  • ✅ Smooth .NET integration
  • ✅ Improved performance from original monolith

What I Learned

  1. Microservices = organizational complexity - Conway's Law is real
  2. Frontend in microservices - you're only as good as your API contracts
  3. Dynamic forms - build a rules engine early
  4. .NET is actually fine - different doesn't mean bad

this project made me appreciate both frontend AND backend deeply. can't do good frontend without understanding the system it connects to.

Related Articles