Claudio Francisco Orellana Andrades Senior Software Engineer
I design and build complete business software, from the database to the interface and the cloud, with fast, iterative delivery.

- Database
- Backend
- Frontend
- Mobile
- Cloud
10 years in technology
- Networking
- 2016 – 2018 · 2 years
- GPS Global
- 2018 – today · 8 years
100,000+ concurrent users: architecture designed for that load; capacity measured with k6 load tests.
The layers of a system
Every business application is built from these five layers. I build all of them. Pick one to see which services and tools belong to it.
- Where your business information is stored and organized.
- Your business rules: it processes, calculates and connects what happens behind the screen.
- What your team and your customers see and use in the browser.
- Apps for iPhone and Android, with phone features such as notifications.
- The servers where everything runs, always available and growing with demand.
Services
One point of contact for the whole cycle: whatever you need built, from the data to the deployment.
- Business web applications
- Custom systems with Vue and TypeScript on the frontend and NestJS or Laravel on the backend, organized in layers so they stay easy to change as they grow. See custom softwareFrontendBackend
- iOS and Android mobile apps
- A single codebase with Capacitor, plus native pieces in Swift and Java when the product calls for them: widgets, Siri shortcuts, biometrics and notifications. See mobile appsMobile
- PWA
- Apps installable from the browser, with offline support and notifications, without going through the app stores. See mobile appsFrontendMobile
- Databases and APIs
- MySQL or PostgreSQL models built to grow, and REST and real-time APIs with typed contracts. See custom softwareDatabaseBackend
- Custom UX/UI
- An in-house design system (tokens, components and states) with dark mode and high contrast. Accessible interfaces, not templates. See custom softwareFrontend
- Infrastructure and deployment
- Docker, Kubernetes and Google Cloud, networking included: the system scales on its own with demand. See infrastructureCloud
How I work
Engineering rigor in short cycles: you see real progress from the start.
We talk
You tell me what you need to build or scale. I reply within 24 hours.
We specify it
Every feature is written up with numbered acceptance criteria before coding.
Short releases
Small, frequent increments that you validate, instead of waiting for the end.
Tested
Every feature is covered by unit and end-to-end tests.
In production
Deployed in containers on the cloud, ready to grow.
Architecture
This is how I build systems that scale on their own.
Load balancing with HAProxy, an API gateway, containerized services on Kubernetes, a message queue for heavy work and autoscaling with KEDA. With this pattern I designed a platform for more than 100,000 concurrent users (measured with k6).
Entry
Cloud
In plain termsSpreads visits so no server gets overloaded and every request reaches its destination.
An HAProxy load balancer distributes traffic and an API gateway routes each request to the right service.
- HAProxy
- API gateway
Services
Backend
In plain termsEach part of the system runs isolated in its own container: it can be updated without stopping the rest.
APIs in NestJS and Laravel, packaged with Docker and deployed as containers on Kubernetes on Google Cloud.
- NestJS
- Laravel
- Docker
- Kubernetes
Messaging
Backend
In plain termsLong tasks, such as a report, run in the background and the user doesn’t wait.
Heavy work is published to a message queue and processed separately, without making the user wait.
- Pub/Sub
Scaling
Cloud
In plain termsWhen more users arrive, servers are added automatically; when demand drops, they are removed.
KEDA adds or removes replicas based on real demand, with no manual intervention.
- KEDA
Data and real time
Database
In plain termsInformation is stored safely and changes show up on screen instantly.
A relational database, Redis for caching and queues, and WebSockets to push every change live to the web, the PWA and the apps.
- MySQL
- PostgreSQL
- Redis
- WebSockets
Frontend architecture
Vue, Pinia and Tailwind are tools. What keeps a large app alive is its architecture.
Layers with one-way dependencies, patterns with contracts and performance measured before optimizing. That is how I structure the apps I build, including rewriting a legacy Vue 2 SPA into Vue 3 with TypeScript, with a screen-by-screen parity plan.
Measured, not eyeballed
Login startup JS683 → 215 KiB−69 %
Frame time while panning the map (95 out of 100 frames, at most)33.4 → 16.7 ms−50 %
Dropped frames while panning the map62 → 3 frames−95 %
Budget enforced on every build: 240 KiB of startup JS and 240 KiB of startup CSS, and a 60 MB memory ceiling.
Measured and ruled out with data: WebAssembly (Rust), SSR and GPU markers. Not everything that sounds faster is.
Design system and accessibility
- Color, typography and motion tokens (120, 180 and 240 ms, two curves) in a single place.
- Visual layers with one rule: glass for navigating, solid surface for reading. Migrated across 385 usages in 197 files.
- Dark mode and high contrast, honoring the system high-contrast mode and the reduced motion and transparency preferences.
- Forms with accessible focus and errors, skeletons while loading and layouts from 360 px up.
What I do on the frontend
Vue in both syntaxes
In my apps I write components with <script setup lang="ts"> and the Composition API. This landing page is my first Vue project in TSX: functional defineComponent and the JSX syntax that comes from React, with the same architecture.
Vue app architecture from scratch
I define layers per domain (types, data access, services, stores, composables and views) and keep the rules written down and enforced by scripts, not just in a document. In a Laravel monolith with Inertia, I generate TypeScript types and routes from the backend.
Migrations without losing features
I rewrite legacy Vue 2 apps into Vue 3 with TypeScript domain by domain, with a screen-by-screen parity plan and a list of the old system’s bugs that I fix along the way.
Hybrid apps: web, PWA, Android and iOS
A single Vue codebase with Capacitor, with platform code isolated in services. I write native pieces in Java and Swift (widgets, session shared with the app and push notifications) and integrate plugins for biometrics and secure storage.
End-to-end online payments
Pending and paid invoices, gateway charges with amount validation, payment by link and blocking of double payments while a charge is in progress. The return from the gateway also works when the app runs as native.
Real time
Private WebSocket channels: each change is applied in place and never backwards, with resync after a long absence and a warning if the connection drops. For heavy reports I use streaming: results appear while the server is still computing.
A network layer that protects the backend
A single HTTP client with typed errors, retries only when safe, automatic failover if the primary server doesn’t respond, timeouts and cancellation. On top of that: caching, deduplication, a concurrency limit and polling that pauses in the background.
Every kind of test, always
In all my projects, backend and frontend, I work with TypeScript and every kind of test: a spec with acceptance criteria before the code, unit tests with a 100% coverage threshold and Playwright e2e per user profile and screen size.
Measured performance
A startup budget that breaks the build if exceeded, heavy SDKs loaded only when needed, typed-array indexes for very large lists and an automatic lightweight mode for modest devices.
Accessible design systems
Color, typography and motion tokens, components with typed variants and a catalog of canonical pieces to avoid duplication. Dark mode, high contrast, reduced motion and layouts from 360 px up.
Client-side security and privacy
Secrets encrypted per platform and state cleared on logout, with a justified allowlist. Pseudonymized analytics with no contact data, and errors reported without tokens or emails.
App layers
Each layer depends only on the ones below.
- ViewOne screen. Assembles the page from components and decides no rules.
- ComponentReusable piece: displays data and reports what happened.
- ComposableOrchestrates an interaction: ties state, data and rules together reactively.
- StoreA domain’s state, with explicit loading and error. One per domain.
- ServiceBusiness rules in plain TypeScript: no Vue, no Pinia, no network.
- RepositoryOne per domain: routes, parameters and the typed raw response. Applies no rules.
- HTTP clientThe single network entry point: typed errors, retries and timeouts.
Swapping an external provider or an endpoint touches one layer, not the whole app.
Patterns I apply
Service layer
- The problem
- Business rules end up scattered across screens, and changing one means hunting for it in several components.
- How I solve it
- I write the rules in plain TypeScript services, with no Vue or Pinia: balances, limits on a charge, assembling an order. I export the class for tests and a single instance for the app; the clock and dependencies come in as parameters.
- What the business gains
- A business rule changes in a single file, and its test flags right away if something breaks.
// Reglas puras: sin Vue, sin Pinia, sin red.export class FacturasService { constructor(private readonly tope = 5_000_000) {} saldo(f: Factura): number { return Math.max(0, f.monto - f.abonado) } total(fs: Factura[]): number { return fs.reduce((s, f) => s + this.saldo(f), 0) } excedeTope(fs: Factura[]): boolean { return this.total(fs) > this.tope }}export const facturasService = new FacturasService()Repository per domain
- The problem
- Each component builds its own URLs and reads the backend JSON its own way, so an API change breaks random screens.
- How I solve it
- Each domain has its repository: it knows the routes, the parameters and the raw shape of the response, unwraps the envelope and returns typed data. No component builds URLs; services map to the model and transport is a single HTTP client.
- What the business gains
- If the backend changes a route, the fix lives in one file, and a script warns if anyone calls the backend outside this layer.
type Sobre<T> = T[] | { data?: T[] } // Rutas y formatos; el transporte es del cliente HTTP.export class RepositorioPedidos { constructor(private readonly http: ClienteHttp) {} async listar(cliente: string): Promise<PedidoCrudo[]> { const r = await this.http.get<Sobre<PedidoCrudo>>( '/pedidos', { cliente }, ) // Normaliza el sobre: siempre un arreglo. return Array.isArray(r) ? r : (r.data ?? []) } cancelar(id: string): Promise<void> { const ruta = '/pedidos/' + encodeURIComponent(id) return this.http.delete(ruta) }}Factory with an interface
- The problem
- The code that uses a piece ends up knowing which concrete class to create and which platform the app runs on.
- How I solve it
- A create… function returns the interface and hides the implementation: it picks native secure storage or the browser’s depending on the platform, or hands back an SDK adapter. Callers receive it as a parameter, with the real one as default, so in tests I pass a fake factory.
- What the business gains
- A single codebase serves web, Android and iOS, and tests swap the real piece for a fake one without touching its callers.
// Quien la usa recibe la interfaz, no la clase.export function crearAlmacenSeguro(): AlmacenSeguro { return esNativo() ? new AlmacenNativo() : new AlmacenWebCifrado()} // La fábrica entra como parámetro, la real por defecto.export function useCobro( crearPasarela: () => PasarelaPago = crearPasarelaReal,) { const pasarela = crearPasarela() const cobrar = (fs: Factura[]) => pasarela.cobrar(fs) return { cobrar }}// En el test: useCobro(() => pasarelaFalsa)Composables
- The problem
- Large components mix data loading, timers and state, and that logic can be neither reused nor tested.
- How I solve it
- I extract each interaction into a composable with a typed contract: it receives its dependencies as functions, composes with others (a refresh that pauses in the background) and cleans up after itself on unmount. The view just wires them together.
- What the business gains
- The same logic serves several screens, such as paying from the account and paying by link, and is tested without mounting the UI.
// La credencial llega como getter: siempre la vigente.export function useCobrosPendientes( token: () => string | null,) { const facturas = ref<Factura[]>([]) const pendientes = computed(() => facturas.value.filter((f) => servicio.saldo(f) > 0), ) async function cargar() { const t = token() facturas.value = t ? await repositorio.listar(t) : [] } // Se compone: un refresco que se pausa en segundo plano. useRefrescoPeriodico(cargar, { cadaMs: 120_000 }) return { facturas, pendientes, cargar }}Adapter with a contract
- The problem
- A provider’s SDK (payments, maps, notifications) leaks through the app, and replacing it means touching every screen that uses it.
- How I solve it
- I wrap external SDKs behind an interface of my own: the rest of the app talks to that contract and tests use a fake implementation. I apply the same idea to the payment gateway: I translate the charge into the provider’s format, validate amount and length limits before sending and interpret the response as a URL or a readable error.
- What the business gains
- Switching providers touches one file, and tests depend on neither the real SDK nor the network.
// Contrato propio: la app no conoce al proveedor.export interface PasarelaPago { cobrar(facturas: Factura[]): Promise<string>}// Un adaptador por proveedor: traduce y valida.export const pasarelaA = (sdk: SdkA): PasarelaPago => ({ async cobrar(fs) { const r = await sdk.crearCobro(aCobroA(fs)) return r.urlPago },})// En los tests, una falsa: sin red ni cobros reales.export const pasarelaFalsa: PasarelaPago = { cobrar: async () => '/pago/ok',}Caching decorator
- The problem
- The same screen requests the same thing several times, and adding caching means touching every consumer.
- How I solve it
- I wrap the existing contract without changing its signature: identical in-flight reads share one request, a semaphore limits concurrency and an idempotency key keeps a double click from duplicating a write. Each behavior is locked in by a test.
- What the business gains
- Less load on the server and faster responses, without rewriting what already works.
// Envuelve la función sin cambiar su firma.export function compartirEnVuelo<T>( pedir: (id: string) => Promise<T>,): (id: string) => Promise<T> { const enVuelo = new Map<string, Promise<T>>() return (id) => { const previa = enVuelo.get(id) // Dos lecturas iguales: una petición, objetos aparte. if (previa) return previa.then(structuredClone) const p = pedir(id).finally(() => enVuelo.delete(id)) enVuelo.set(id, p) return p }}Real time that merges
- The problem
- With WebSockets the screen flickers when whole lists are replaced, and a message that arrives out of order can show stale data.
- How I solve it
- Each message carries only the record that changed, and the store applies it in place instead of replacing the list. If it arrives out of order and is older than the current one, I drop it. Since the channel keeps no pending messages, after a long absence I reload the data, and if the connection has been down for a few seconds, the app says so.
- What the business gains
- Live data with no jumps or rollbacks, plus a safety net for whatever the channel failed to deliver.
const pedidos = reactive(new Map<string, Pedido>()) // Cada mensaje trae un registro: se aplica en su lugar.canal.on('pedido', (nuevo: Pedido) => { const actual = pedidos.get(nuevo.id) // Nunca hacia atrás: uno viejo no pisa uno nuevo. if (actual && nuevo.fecha <= actual.fecha) return pedidos.set(nuevo.id, { ...actual, ...nuevo })})// El canal no tiene buzón: tras una ausencia larga, releo.watch(enPrimerPlano, (visible) => { if (visible && ausenciaMs() >= 5 * 60_000) void releer()})Typed variants
- The problem
- Buttons and states get copied by hand, and every screen ends up with its own version.
- How I solve it
- Each variant is a key in a typed Record: I took the idea from CVA without adding the dependency, and if a style is missing, it doesn’t compile. When the key comes from the backend, I use a fallback value so a new state doesn’t break the screen.
- What the business gains
- A consistent design system enforced by the compiler, not by the team’s memory.
type EstadoPago = 'pendiente' | 'pagado' | 'fallido'type Insignia = { etiqueta: string; clase: string }// Si falta una variante, no compila.const ESTADOS: Readonly<Record<EstadoPago, Insignia>> = { pendiente: { etiqueta: 'Pendiente', clase: 'bg-aviso' }, pagado: { etiqueta: 'Pagado', clase: 'bg-exito' }, fallido: { etiqueta: 'Fallido', clase: 'bg-alerta' },}// Un estado nuevo del backend no rompe la pantalla.export const insignia = (estado: string): Insignia => Object.hasOwn(ESTADOS, estado) ? ESTADOS[estado as EstadoPago] : { etiqueta: estado, clase: 'bg-neutro' }Explicit states
- The problem
- With a couple of booleans, the app says “you have no invoices” when the request actually failed.
- How I solve it
- I give a name to what a couple of booleans blur together: not fetched yet, first load, confirmed empty, first load failed, and refresh failed with previous data. If a refresh fails, I keep the data and flag it as possibly stale. When a flow has phases, I model them as a union of literals.
- What the business gains
- The app never claims what it doesn’t know, and users can trust what they see.
// Cada bandera responde una sola pregunta.export function useCargaFacturas() { const consultado = ref(false) // ¿ya pregunté? const respondio = ref(false) // ¿contestó alguna vez? const error = ref<string | null>(null) const facturas = ref<Factura[]>([]) const cargandoInicial = computed(() => !consultado.value) // «No tienes facturas» solo si el servidor lo confirmó. const sinFacturas = computed(() => respondio.value && !error.value && !facturas.value.length, ) // Un refresco fallido no borra lo que había: lo marca. const desactualizada = computed( () => respondio.value && error.value !== null, ) return { facturas, cargandoInicial, sinFacturas, desactualizada, }}Smooth large lists
- The problem
- With tens of thousands of rows, filtering creates one object per item and the UI stops responding.
- How I solve it
- I stop creating one object per row: an index stores the filtered positions in an Int32Array and each item’s group as a number in a Uint16Array, and the list renders only the visible window. Heavy network work, such as resolving addresses for the visible page, runs in a Web Worker.
- What the business gains
- The UI stays smooth as the list grows: filtering no longer creates an object per row.
// Posiciones, no objetos: un índice por coincidencia.export function filtrar(textos: string[], q: string) { const salida = new Int32Array(textos.length) let n = 0 for (let i = 0; i < textos.length; i++) { if (textos[i]?.includes(q)) salida[n++] = i } return salida.subarray(0, n)}// La vista materializa solo el tramo visible.export const visibles = computed(() => { const { desde, hasta } = ventana.value return [...posiciones.value.subarray(desde, hasta)] .map((i) => filas[i])})Stack
The tools I use, by area. How I organize them in an app is in the frontend architecture.
Languages
- TypeScript
- PHP
- Python
- Java
- Swift
Frontend
- Vue 3 · Composition API
- script setup and TSX
- Strict TypeScript
- Pinia per domain · Pinia Colada
- Vite 8
- Tailwind v4 with tokens
- Web Workers
Backend
- NestJS
- Laravel
- WebSockets
- Redis
Databases
- MySQL
- PostgreSQL
Mobile
- Capacitor
- Swift · WidgetKit
- Java (Android)
- PWA · service workers
- Firebase
Infrastructure and cloud
- Docker
- Kubernetes
- KEDA
- HAProxy
- Google Cloud
- Pub/Sub
- Cloud Run · Cloud Build
Testing
- Vitest
- Playwright
- k6
Tooling
- Native TypeScript 7 (tsgo)
- oxlint and oxfmt
- Rolldown (Vite 8)
- ESLint
- Workbox (PWA)
This very page
It is my first Vue project in TSX: Vue 3.6 on Vite 8 and Tailwind CSS 4, built with the new generation of native tooling. Every change ships with its spec, unit tests and e2e, just like in production.
The generational leap
JavaScript tooling is being rewritten in native languages, Go and Rust. The new tools do the same job as the old ones in a fraction of the time: linting, formatting and compiling go from seconds to fractions of a second, and that changes how you work.
TypeScript 7 (tsgo)Go
Instead of tsc, the TypeScript compiler up to version 6
Microsoft rewrote the compiler, which was written in TypeScript and ran on JavaScript, as native Go code that uses every core: according to Microsoft, about ten times faster. I use it with a bridge that keeps the TypeScript 6 API for the Vue tooling.
0.6 stype-checking the whole project
oxlintRust
Instead of ESLint, for almost every rule
Checks the code with hundreds of ESLint-compatible rules. ESLint is kept only for Vue-specific rules, and the overlapping ones are turned off.
0.2 sthe whole repo; ESLint takes 1.2 s
oxfmtRust
Instead of Prettier
Formats with the same style as Prettier, which isn’t even installed anymore.
7 ms44 files formatted
Rolldown (in Vite 8)Rust
Instead of Rollup and esbuild
Vite 8 bundles with a single engine instead of two. Rolldown and Oxc, the project behind oxlint and oxfmt, come from the same team as Vite.
0.25 sproduction build
Times measured on my machine against this repository.
How it reaches the internetCloud
The code lives on GitHub. Every time I push a change to the main branch, Google Cloud checks it, packages it and publishes it on its own, without me touching a server.
I push the change
GitHub
I push to the main branch after the unit and e2e tests, the lint and the build pass on my machine.
It gets checked
Cloud Build
A Google Cloud trigger detects the change and type-checks the TypeScript. If anything fails, nothing is published.
It gets packaged
Docker
It builds an image in three stages: install, compile with Vite and keep only the final files on nginx, without administrator privileges.
It gets stored
Artifact Registry
The image is stored tagged with the commit hash: every version can be identified and restored.
It goes live
Cloud Run
Cloud Run shifts traffic to the new version once it is ready, with no downtime, and the claudioorellana.cl domain points to it.
What Cloud Run is
Cloud Run is Google’s serverless platform for containers. I hand it a Docker image and Google takes care of the rest: servers, scaling and the HTTPS certificate.
Its strengths
- Scales on its own: more visits add instances (here, up to five) and no visits scale it to zero, so you pay for what you use.
- No servers to maintain: no patches or operating system, just the container.
- HTTPS and a custom domain, with the certificate managed by Google.
- Every deployment is an immutable revision: if something goes wrong, roll back to the last one.
- It is a standard container: the same image runs on my machine, on Kubernetes or another cloud.
Why this stack
I choose tools that let me move fast without losing solidity: the native speed of Go and Rust to work, the safety of TypeScript to write and the flexibility of Vue to adapt to each project.
- The speed of Go and Rust
- The TypeScript compiler in Go and the linter, formatter and bundler in Rust go through the whole project in under a second. Changing, testing and shipping become instant, and that agility shows in every release.
- A language that protects the business
- With TypeScript, every piece of data and every contract has a type: the error shows up while writing, not in production. And one language on the frontend and in the NestJS services makes crossing layers effortless.
- Vue, a progressive framework
- Vue grows with the problem: it can be a library on a single page or the core of a full app. And it speaks every syntax companies ask for: Options API, Composition API with <script setup> and TSX with React’s JSX syntax.
- Case by case, with experience
- This stack can handle almost anything, which is why I choose carefully. Each project defines its architecture, its tools and its level of complexity, not the other way around.
The possibilities are endless; experience lies in choosing.
About me
I started out in networking. Since 2018 I have been at GPS Global, where I now lead the Technology area: I make the architecture decisions, build, and answer for a web and mobile platform used by thousands of customers.
Eight years running the same system taught me what a demo doesn’t show: code has to be changeable without fear, performance is measured before it is optimized, and every feature reaches production with its tests.
Education
- Computer Engineer
- Bachelor’s degree in Information and Communication Technologies
INACAP, sede Maipú
Career path
GPS Global2018 – present
Senior Software Engineer · I lead the Technology area
Web platform and iOS/Android app for GPS fleet tracking, in production for thousands of customers. I own its architecture, from the database to the cloud, and every module that reaches production.
Platform modules (8)
Real-time tracking
Fleet map, routes, events and a 360° profile for each vehicle.
Alerts and incidents
Geofences, speeding, stops and temperature, with incident handling shared across operators.
Reports and analytics
Totals and charts, PDFs with the customer’s branding, fuel consumption, idling cost and automated reports.
Vehicle maintenance
Legal deadlines, odometer and workshop log.
Drivers
iButton identification and a profile for each driver.
Hardware health
Status of every GPS device and failure prediction.
Native mobile app
Priority panic button, home screen widgets, Siri shortcuts and location sharing.
Accounts and payments
Account management, billing and online payments.
Networking companies2016 – 2018
Networking and infrastructure
My first two years in IT, in the networking industry.
Let's talk about your project
Tell me what you need to build or scale. I reply within 24 hours, from Santiago, Chile.