loading portfolio… 0%
Home Why Me Case Studies Thesis API Resume Match Me Contact
OPEN TO JUNIOR ROLES — RELOCATING WORLDWIDE

Berke TayfunAkseki.

$

CS + Management graduate from LUISS Guido Carli, Rome (Jul 2026). I build systems end to end: TypeScript/Node services on AWS, Airflow/PySpark/dbt data pipelines, containerized FastAPI APIs — all with tests and CI. Nine projects, every repo public on GitHub. Toggle Engineering Mode ↗ for architecture diagrams and design decisions.

0Shipped projects
0Live listings indexed
0Regions analysed in R
0Languages spoken
↓ type here · try 'ls'
berke@akseki: ~/portfolio

Executable value// executable_value

Why hire me?

Four concrete reasons, not adjectives.

🎓

Dual-track tech + strategy degree

Management and Computer Science at LUISS Rome — I write production code and can read a P&L. Thesis used difference-in-differences econometrics on real Eurostat data.

⚙️

End-to-end system ownership

I don't stop at scripts. Energy Connect runs as separate API and worker services behind a PostgreSQL queue, with retries, circuit breakers and AWS CDK infrastructure; Jobscope and the RAG assistant ship as containerized FastAPI services with JWT auth and GitHub Actions CI.

📊

Analytics depth across 3 stacks

Python for ML pipelines, R for econometric panel analysis, SQL for storage — plus Power BI and KNIME from client-facing team projects.

🌍

Zero-friction relocation — worldwide

Ready to relocate anywhere for the right role. Non-EU — I say it upfront and target employers with sponsorship experience. Available immediately.

Proof of work// case_studies

Case studies

Problem → solution → result. Toggle Engineering Mode for architecture, trade-offs, and what broke along the way.

~/projects/energy-connect-platformBackend · Distributed Systems · CloudSource ↗

Energy Connect Platform

TypeScript/Node.js connectivity layer for EV chargers and vehicles — my largest system

ArchitectureSeparate API and worker processes over a durable PostgreSQL command queue
ReliabilityIdempotency keys, exponential-backoff retry, circuit breaker, dead-letter state
CloudAWS CDK → CloudFormation: ECS/Fargate, RDS, CloudWatch alarms, autoscaling

Problem. An energy platform sits in front of OEM APIs that time out, rate-limit and fail in ways the product above them must never notice. One flaky vendor cannot be allowed to degrade the whole platform — and a retried request must never start a car charging twice.

Solution. A connectivity layer built to isolate failure. Commands are written to a durable PostgreSQL queue with an idempotency key, workers claim rows using FOR UPDATE SKIP LOCKED so two workers never pick up the same command, and every provider call is wrapped in retry plus circuit-breaker logic. Structured logs, Prometheus metrics and an SLO snapshot make failure visible; a React operations dashboard shows device state, queue depth and SLO health. Five backend tests run in GitHub Actions against a live PostgreSQL service container.

[React ops dashboard]      [client / integrator]
             \                    /
              v                  v
        [Node.js API — TypeScript]
                    | enqueue command + idempotency-key
                    v
        [PostgreSQL — devices + command_queue]
                    ^ FOR UPDATE SKIP LOCKED
                    |
           [Node.js worker pool]
                    | retry (exp. backoff) + circuit breaker
                    v
        [Simulated OEM A]   [Simulated OEM B]

 observability: structured JSON logs · Prometheus metrics · SLO snapshot
 infra: AWS CDK > CloudFormation > ECS/Fargate · RDS · CloudWatch alarms

// engineering decisions

  • Why a PostgreSQL queue instead of SQS or Kafka? The command state and the queue state have to stay consistent with each other. Keeping both in one transactional database removes a whole class of "the message was sent but the row was never written" bugs, and at this scale a broker would be infrastructure I have to justify rather than use.
  • Why FOR UPDATE SKIP LOCKED? It is the standard way to let several workers drain one table concurrently: each worker locks the rows it claims and skips whatever is already locked, so throughput scales with worker count instead of serialising behind a single lock.
  • Why a circuit breaker on top of retries? Retrying against a provider that is already down turns one outage into a self-inflicted load test. The breaker trips after a failure threshold and fails fast until the provider recovers.
  • Why idempotency keys at the API edge? Clients retry. Without a uniqueness constraint on the key, a network hiccup silently becomes two START_CHARGING commands — the exact bug that is invisible in tests and expensive in production.
  • Why AWS CDK rather than raw CloudFormation? The stack is typed TypeScript, so the infrastructure is reviewed by the same compiler as the application. cdk synth still emits plain CloudFormation.

// lessons learned

  • Reliability code is only real if it is tested against failure. The retry and circuit-breaker units are tested with deliberately failing providers — proving the breaker actually opens is the whole point.
  • Splitting API and worker changed the design. Once the two processes cannot share memory, every piece of coordination has to become explicit state in the database — which is exactly what made the queue design necessary.
  • Future work: real OEM adapters behind the existing provider interface, Grafana dashboards checked into the repo, and blue/green deploys on ECS.
TypeScriptNode.jsPostgreSQLReactDockerAWS CDKPrometheusGrafana
~/projects/analytics-data-platformData Engineering · OrchestrationSource ↗

Analytics Data Platform

End-to-end batch pipeline: Airflow → PySpark → Parquet → PostgreSQL → dbt

OrchestrationAirflow DAG drives ingest, transform, load and dbt build in order
TransformPySpark schema validation, typing and deduplication into Parquet
Modellingdbt staging + daily-sales mart with schema tests

Problem. "I know SQL" and "I can build a data platform" are different claims. The second one needs orchestration, a transformation engine, a warehouse target, a modelling layer and tests that fail loudly when the data is wrong.

Solution. A reference pipeline that runs the whole loop: raw order events are validated, typed and deduplicated in PySpark, written as Parquet, loaded into a PostgreSQL warehouse, then modelled in dbt as a typed staging layer and a daily-sales fact table with data-quality tests. Airflow orchestrates the sequence, Docker Compose reproduces the environment locally, and GitHub Actions validates the project on every push.

[data/raw/orders.csv]
        | ingest
        v
[PySpark — schema validation · typing · dedup]
        | write
        v
[Parquet — partition-friendly columnar]
        | load
        v
[PostgreSQL warehouse]
        |
        +-- dbt staging: stg_orders  (typed, cleaned)
        |
        +-- dbt mart:    fct_daily_sales  + schema.yml tests

 orchestration: Airflow DAG · env: Docker Compose · CI: GitHub Actions

// engineering decisions

  • Why Parquet between Spark and Postgres? Columnar storage keeps the transform output cheap to re-read and re-load, and it means a failed load can be retried without re-running the Spark job.
  • Why dbt on top of SQL I could have written by hand? dbt gives the modelling layer the things raw SQL files do not: dependency-ordered builds, a documented schema, and tests that run as part of the pipeline rather than as a separate good intention.
  • Why deduplicate in Spark rather than in the warehouse? Deduplicating before the load keeps the warehouse honest — the staging table is then a faithful record of what arrived, not a place where cleaning silently happens.

// lessons learned

  • Orchestration is where the real design lives. Each step is easy alone; making them idempotent and re-runnable in a fixed order is the part that takes thought.
  • Tests belong in the model layer. A mart that silently produces wrong totals is worse than one that fails — dbt schema tests turn a data bug into a build failure.
  • Future work: incremental dbt models, partition-aware Spark writes and freshness checks on the source data.
Apache AirflowPySparkdbtPostgreSQLParquetDockerGitHub Actions
~/projects/jobscopeML Pipeline · BackendSource ↗

Jobscope

Swiss / EU job-market tracker — built to solve my own job search

PipelineRemoteOK + Adzuna APIs → FastAPI → TF-IDF engine → SQLite
ScoringTF-IDF vectorisation + cosine similarity, ranked 0–100
Scale200+ live listings, URL-normalised dedup

Problem. Tracking junior roles across Swiss and EU job boards manually meant duplicates, stale posts, and no way to rank fit.

Solution. An automated aggregation pipeline: fetch from two APIs, normalise URLs to kill duplicates, score every posting against my candidate profile with TF-IDF cosine similarity, and serve everything through a JWT-authenticated FastAPI backend in Docker.

[RemoteOK API]   [Adzuna API]
       \             /
        v           v
   [Ingestion — Python requests]
              |
   [Dedup — normalised URL hash]
              |
      [TF-IDF matrix] <──> [candidate profile doc]
              |   (cosine similarity → score 0–100)
              v
        [SQLite] <── [FastAPI + JWT] <── client

// engineering decisions

  • Why FastAPI? Async I/O for concurrent external API calls without blocking; Pydantic gives free payload validation.
  • Why SQLite? Read-heavy workload with batch writes — a Postgres container would add ops cost for zero benefit at this scale.
  • Why TF-IDF over LLM embeddings? Job descriptions are keyword-dense; sparse vectors get comparable ranking quality at zero compute cost and no API dependency.

// lessons learned

  • Duplicates were the real enemy. The same posting appears across boards with different URLs — normalising before hashing fixed silent double-counting.
  • Future work: Postgres migration, scheduled refresh via cron, and response caching on the scored-jobs endpoint.
FastAPIscikit-learnSQLiteDockerJWTPydantic
~/projects/causal-labStatistics · ExperimentationSource ↗

Causal Lab

A/B testing & difference-in-differences toolkit

Hypothesis testingTwo-proportion z-test with confidence intervals
Data qualityAutomated sample-ratio-mismatch (SRM) detection
Causal inferenceTwo-way fixed-effects DiD, clustered SEs

Problem. Most portfolio ML projects are prediction models — the causal-inference and experimental-design side of data science (the part that decides whether a launch actually caused a metric to move) is usually skipped entirely.

Solution. Built the toolkit I wish existed: power analysis to size a test correctly, a two-proportion z-test with proper confidence intervals, an automated SRM check that flags broken randomization before anyone trusts a p-value, and a difference-in-differences estimator for when you can't randomize at all — the same estimator family as my econometrics thesis, applied to a generic experimentation setting.

// engineering decisions

  • Why cluster standard errors in the DiD model? Without clustering at the unit level, SEs are understated whenever outcomes correlate within a unit over time — which overstates significance. Easy mistake, strong interview signal to avoid.
  • Why simulate ground-truth effects instead of just testing on real data? Every method is validated against synthetic data with a known injected effect, so tests prove the statistics actually recover the truth — not just that the code runs.

// lessons learned

  • Testing the null, not just the alternative, caught a real bug. A test that correctly detects a real effect can still silently violate alpha under the null — simulating "no true effect" and checking the false-positive rate caught this.
  • statsmodels emits a rank-deficiency warning on the fixed-effects DiD spec with many unit dummies + clustered SEs — a known quirk, not a bug; a production version would use within-transformation demeaning instead.
PythonSciPystatsmodelsFastAPIStreamlit
~/projects/sql-analytics-labSQL · Product AnalyticsSource ↗

SQL Analytics Lab

Cohort retention & funnel analysis in raw SQL

Funnel analysisLAG(), FIRST_VALUE() for step-over-step conversion
Cohort retentionMulti-CTE weekly retention by signup cohort
Growth metricsRunning totals, RANK() vs DENSE_RANK()

Problem. The most common SQL interview question shape at Google, Meta, and Amazon is some version of "compute N-week retention by cohort" or "rank X by Y, handling ties correctly" — the kind of query that's easy to get subtly wrong.

Solution. Wrote production-style analytical SQL — no ORM, no pandas groupby standing in for what SQL should do — against a synthetic app-usage dataset: funnel conversion, weekly cohort retention, running totals with rolling averages, and country-level ranking demonstrating the RANK vs DENSE_RANK distinction on ties.

// engineering decisions

  • Why LAG() over a self-join for funnel drop-off? A self-join works but scales poorly and reads worse. LAG() is the idiomatic tool for "compare this row to the previous row" and every modern engine optimises it well.
  • Why hardcode funnel step order instead of alphabetical? Funnel steps have a real business sequence that doesn't match alphabetical order — hardcoding the CASE-based sequence prevents a silent, hard-to-notice bug.

// lessons learned

  • My first comment-stripping logic silently dropped every query. It checked if an entire multi-line statement started with "--", which was always true since each one opens with a comment header. Fixed by stripping comment lines before splitting statements.
  • Synthetic retention data needs calibration to look real. Gating all repeat-session generation behind only "fully activated" users produced retention numbers far below real benchmarks — tiering by funnel depth fixed it.
SQLWindow FunctionsCTEsSQLiteStreamlit
~/projects/regulatory-rag-assistantApplied AI · DevOpsSource ↗

Regulatory RAG Assistant

Retrieval-augmented Q&A over regulatory PDF documents

IngestionPDF parsing → chunking → embeddings
RetrievalFAISS vector index, semantic similarity search
DeliveryFastAPI REST + Streamlit UI · Docker + GitHub Actions CI

Problem. Regulatory documents are long, dense, and painful to query — keyword search misses context entirely.

Solution. A full RAG loop: ingest PDFs, chunk text, embed chunks into a FAISS index, retrieve semantically relevant passages per query, and serve results via REST plus an interactive Streamlit interface. CI runs lint and tests on every push.

[PDF / TXT docs]
      | parse + chunk
      v
[Embedding model] ──> [FAISS vector index]
                            | top-k semantic search
                            v
                 [FastAPI /query endpoint]
                     /            \
           [Streamlit UI]    [REST consumers]

 CI: GitHub Actions — lint + test on push · Docker image build

// engineering decisions

  • Why FAISS? In-memory index gives millisecond retrieval with no external vector-DB service to run or pay for.
  • Why decouple UI from API? Streamlit is for demos; the REST layer means any client can consume retrieval independently.

// lessons learned

  • Chunking strategy matters more than the model. Naive splits break clauses mid-sentence; overlap between chunks preserved retrieval context.
  • CI from day one caught dependency breakage early — cheaper than debugging a broken container later.
FastAPIFAISSStreamlitDockerGitHub Actions
~/projects/labyscapeDistributed Systems · Game DevRepo private — in dev

Labyscape

2–8 player co-op horror game — sole developer, targeting Steam

EngineUnity 6 URP · C# · Netcode for GameObjects
World genProcedural 18×18 maze via depth-first search
NetworkingHost-authoritative sync, health/respawn state machines

Problem. Real-time multiplayer demands consistent world state across clients — every player must see the same maze, the same enemies, the same damage events.

Solution. Host-authoritative architecture: the host generates the DFS maze at session start and replicates it; NavMesh enemy AI, health and respawn systems synchronise through NetworkObject state. Modular day/night cycle drives difficulty.

// engineering decisions

  • Why host-authoritative? For a co-op game, dedicated servers are overkill — host authority prevents cheating-by-desync at zero infra cost.
  • Why DFS for maze gen? Guarantees a fully connected maze with exactly one path between any two cells — no unreachable rooms, ever.

// lessons learned

  • Sync what changes, not what exists. Replicating the maze seed instead of the full grid cut join-time payload massively.
  • Client prediction is hard — small desyncs in enemy pathing taught me why authoritative state design comes first.
Unity 6C#NetcodeNavMeshBlender
~/projects/churn-ml-api-dashboardML · Applied StatsSource ↗

Churn Prediction — ML API + Dashboard

Customer-churn classifier served via API and dashboard

Pipelinescikit-learn Pipeline for preprocessing + training
EvaluationAccuracy, ROC-AUC, confusion matrix
ServingFastAPI prediction endpoint + Streamlit dashboard

Problem. A trained model in a notebook is not useful to a business stakeholder — it needs to be queryable and visual.

Solution. Wrapped the trained classifier behind a FastAPI endpoint for programmatic access, and built a Streamlit dashboard so a non-technical reviewer can explore predictions and metrics directly.

scikit-learnFastAPIStreamlitjoblib
~/projects/django-crud-auth-appFull-Stack · AuthSource ↗

Django CRUD + Auth App

Full-stack book catalogue with user authentication

AuthDjango user registration, login, logout
Data modelFull CRUD over books/authors with image uploads
StorageSQLite, server-rendered templates

Problem. Needed a from-scratch full-stack app to prove I understand the request/response cycle without relying on a framework's admin panel for everything.

Solution. Built views, templates, and models by hand: authentication guards on CRUD routes, image upload handling, and a clean server-rendered UI.

DjangoSQLiteHTML/CSS

Empirical rigor// thesis.R

Bachelor's thesis

Econometric research on real EU data — the analytical foundation behind the engineering.

"Tourism Dependence and Regional Macroeconomic Resilience in Italy: The Economic Effects of the COVID-19 Shock"

Difference-in-DifferencesEvent StudyPanel DataREurostat / ISTAT

Panel analysis of 21 Italian NUTS 2 regions over 2015–2024 using a continuous DiD framework with event-study specification. Identified structural labour-market hysteresis as the mechanism behind persistent post-pandemic divergence in high-tourism regions. Supervisor: Prof. Diletta Topazio, LUISS Guido Carli.

Yit = α + β(Postt × Treatmenti) + γXit + δi + λt + εit

β estimates the differential post-shock effect on tourism-dependent regions, with region (δ) and time (λ) fixed effects.

event_study.R — employment index, 21 NUTS-2 regions

tourism-dependentcontrol
80859095100105 COVID shock 2015201620172018201920202021202220232024

# Parallel pre-trends → sharp 2020 divergence → asymmetric recovery.
# Tourism-dependent regions never return to counterfactual = structural hysteresis. Reconstructed from thesis findings.

21NUTS 2 regions over a 10-year panel (2015–2024)
2Original propositions: Tourism Recovery Asymmetry & Structural Hysteresis
30Grade in Statistics — the top score in the Italian system
RFull data preparation and estimation pipeline built in R

REST paradigm// GET /api/resume

GET /api/resume

The CV, as a backend engineer would serve it.

GET berketayfunakseki.com/api/resume200 OK
{
  "candidate": "Berke Tayfun Akseki",
  "education": {
    "institution": "LUISS Guido Carli University, Rome",
    "degree": "BSc Management and Computer Science (L-18)",
    "graduated": "2026-07"
  },
  "stack": {
    "languages": ["Python", "TypeScript", "SQL", "R", "C#"],
    "backend": ["Node.js", "FastAPI", "Django", "PostgreSQL", "React"],
    "data_eng": ["Airflow", "PySpark", "dbt", "Parquet", "Pandas"],
    "ml": ["scikit-learn", "FAISS", "statsmodels", "Power BI"],
    "cloud_devops": ["AWS CDK", "ECS/Fargate", "RDS", "Docker", "GitHub Actions", "Prometheus"]
  },
  "languages_spoken": { "tr": "native", "en": "C1", "fr": "B2", "it": "B1" },
  "relocation": "worldwide",
  "shipped_projects": 9,
  "available": true
}

Try my work on me// tfidf_match(jd, me)

Paste your job description.
See if I match.

This runs the same TF-IDF matching logic as my Jobscope project — right here in your browser, against my real profile. My code, scoring me. No data leaves this page.

0MATCH

Keyword-frequency matching — same core idea as github.com/berketayfunakseki/jobscope

Get in touch// open_socket()

Let's build
something.

Actively looking for junior roles — anywhere in the world. If you're hiring, I'd love to talk.

Available now — local time Based in İzmir · open worldwide
✓ Offer extendedyou ran 'sudo hire-me' — let's make it official → berketayfunakseki@gmail.com