# Djamel Bougouffa: Full Content > Full-Stack Software Engineer based in Paris, France. TypeScript, React, Node.js, Rust, MCP, RAG, agentic AI. > Portfolio: https://djamel-bougouffa.com --- # AI Writes the Code Now, Which Is Exactly Why You Should Master Architecture URL: https://djamel-bougouffa.com/blog/why-developers-should-learn-architecture-ai-era/ Date: 2026-07-16 Tags: AI, Software Engineering, Tech Leadership > AI didn't make developers obsolete. It moved the value. Typing code is being commoditized; deciding how the system should be shaped is not. Here's why architecture and craft just became the skills that appreciate, and why the 'classic' implementer should start leveling up now. For most of my career, being a good developer meant being good at *producing* code. Fast, correct, idiomatic. That skill still matters, but it's quietly stopped being the one that sets you apart. An agent now produces code fast, correct, and idiomatic on demand. The part of the job that got scarce, and therefore valuable, is the part that decides *what should be produced and how it should be shaped*. That's not a small shift. It relocates where a developer's value lives: from the keyboard to the design. And it has an uncomfortable implication for a whole category of developers who built their identity on implementation speed. This is the case for why architecture and craft are the skills that just appreciated, and why "classic" developers should start learning them now rather than later. ## AI is good at exactly the part that was never the hard part Here's the thing we don't say often enough: writing a function was rarely the hard part of software. Knowing *which* function, where it belongs, what it should and shouldn't touch, and how it fits the rest of the system: that was always the hard part. AI is spectacular at the first and structurally weak at the second. An agent generates locally-plausible code. Ask it for a feature and it will produce something that looks right and often works. What it does not have is taste at the system level: a sense of what to protect, which coupling will hurt you in six months, where a boundary should go, what the domain actually means underneath the tickets. It optimizes for "make this task pass," not "keep this system coherent." (I've watched that exact gap [break a ten-year-old business rule in production](/blog/software-craftsmanship-ai-era).) So the capability didn't disappear. It inverted. The cheap thing became abundant. The expensive thing, judgment about structure, became the bottleneck. And bottlenecks are where value concentrates. ## Architecture is the steering wheel Here's the practical reason this isn't abstract career philosophy. If you can't describe the shape you want, you can't direct an agent. You can only accept whatever it hands you. Working with a coding agent well is an act of *direction*. You tell it: put the business logic in a pure core, keep the Slack call behind a port, don't let the persistence layer leak into the domain. That instruction is only available to you if you understand ports and adapters, coupling, cohesion, where boundaries belong and why. [Architecture is what makes an agent safe and fast](/blog/hexagonal-architecture-ai-agents), but only in the hands of someone who can name the target. The developer who knows architecture treats the agent as a very fast implementer of *their* design. The developer who doesn't treats the agent as an oracle, accepts its output, and slowly accumulates a system nobody understands. Same tool. Opposite outcomes. The differentiator is entirely the human's design literacy. That's the reframing: AI didn't replace the architect's judgment. It made that judgment the interface through which everything else gets built. ## The uncomfortable part for the "classic" developer I want to be direct, because vague optimism doesn't help anyone plan a career. If your value to a team was *implementing tickets that someone else designed* (turning a well-specified task into working code, quickly), that is precisely the layer AI is absorbing fastest. Not because you're not good; because that's the part of the work that's most compressible into "generate plausible code for a clear spec." The implementer who only implements is standing exactly where the water is rising. That's not a doom prophecy. It's a direction sign. The move is *up a layer*: from "I can build what I'm told to build" to "I can decide what should be built and how it should be structured." From implementer to designer. From code-writer to code-*director*. And the skills that get you there aren't more typing speed or another framework: they're the durable, unglamorous ones the industry has undervalued for years because they don't demo well. The people who spent the last decade dismissing "architecture astronauts" and "craft is overkill, just ship" are about to find that the overkill was the moat. ## What "learn craft" actually means (not buzzwords) "Get into craftsmanship" is easy to say and easy to turn into a reading list you never apply. Concretely, here's what appreciates in value: - **Boundaries and coupling.** Ports and adapters, dependency direction, what belongs in the domain versus the edges. This is the vocabulary you steer an agent with. Start here. - **Testing as a design tool, not a chore.** Not chasing coverage, but using tests to pin down behavior and to catch the *silent* changes an agent makes. The test that would have caught the broken business rule is a design skill, not a QA task. - **Domain modeling.** Understanding the business deeply enough to name things correctly. Ubiquitous language isn't ceremony; it's the map you and the agent both navigate by. - **Reading and reviewing code critically.** The single most important skill in an AI-heavy workflow is telling *good* code from *plausible* code. If you can't spot a confidently-wrong diff, the agent's speed is a liability, not leverage. - **System-level thinking.** How a change ripples. What to protect. What "coherent" means for this codebase specifically. This is the taste AI doesn't have, and it's learnable: through exposure, reading great codebases, and getting things wrong on purpose in low-stakes places. None of that is a new hotness. All of it is what senior engineers have quietly always done. The difference is that it's no longer the *hidden* part of the job behind the visible act of typing. It *is* the job now. ## The optimistic version (because it is optimistic) I don't think this is a bad time to be a developer. I think it's the best time to be a developer *who can architect*, and a nervous time to be one who only implements. And those are two very different sentences. For anyone willing to level up, the leverage is extraordinary. An architecturally-literate developer with a good agent is doing the work of a small team: they design, the agent implements, they review, they steer. The ceiling on what one person can build just went up dramatically, but only for people whose skill is deciding the shape, not filling it in. So if I could give one piece of career advice to a "classic" developer right now, it would be short: **stop competing with the agent on the thing it's better at.** It types faster than you and never gets tired. Move to the thing it can't do: hold the whole system in your head, know what to protect, decide the structure, and tell good from plausible. Learn architecture. Learn craft. Not because it's virtuous, but because it's the part of your job that just became the whole job. *If you want the concrete version of "structure your code so an agent thrives," I wrote about [why hexagonal architecture is the best gift you can give an AI agent](/blog/hexagonal-architecture-ai-agents). And for the war story behind all of this, [here's how AI broke our production code, and the craftsmanship rule that would have prevented it](/blog/software-craftsmanship-ai-era).* --- # Hexagonal Architecture Is the Best Gift You Can Give an AI Agent URL: https://djamel-bougouffa.com/blog/hexagonal-architecture-ai-agents/ Date: 2026-07-08 Tags: AI, Software Engineering, Architecture > Ports and adapters weren't designed for AI. But isolating your business logic behind clean boundaries turns out to be the single most effective way to let a coding agent work fast without wrecking your domain. Here's why, with a real Java before/after. We adopted hexagonal architecture for the humans who would read the code later. It turns out we also built it for a reader we didn't see coming: the AI agent working on the codebase today. I didn't plan this. I noticed it. The same coding agent, given the same kind of task, behaved completely differently depending on the shape of the code it landed in. In a tangled codebase it was a liability. In a cleanly layered one it was genuinely fast and genuinely safe. The difference wasn't the model, the prompt, or the tooling. It was the architecture. This is the case for why **ports and adapters is the best gift you can give an AI agent**, and the honest limits of that claim. ## The same task, two codebases Give an agent this task: *"Add a Slack notification when an order ships."* In a tangled service, the shipping logic, the persistence, and the outbound calls all live in one 600-line class. The agent reads it, decides the natural place to hook in is right after the status update, and edits there. It works. It also quietly moves a line, changes when the `shippedAt` timestamp is set, and now a ten-year-old billing rule that keyed off that timestamp fires a day early. No test caught it, because the test asserted "an email is sent," not "the timestamp is untouched." It surfaces in production. (If that failure mode sounds familiar, it's because [I've watched an agent silently break an undocumented business rule in production](/blog/software-craftsmanship-ai-era): a different rule, the same shape of mistake.) In a hexagonal codebase, the same task is boring in the best way. There's a `NotificationPort` interface. The agent implements a `SlackNotificationAdapter`, wires it in, and never touches the domain at all. The business rule about `shippedAt` lives in a pure core the agent had no reason to open. The blast radius of the change was bounded *by the architecture*, before the agent typed a character. That's the whole thesis. The rest of this post unpacks why the second story happens by design, not by luck. ## Why hexagonal works *for an agent* specifically Hexagonal architecture is old news for humans. What's new is that its exact properties happen to be the ones an AI agent needs most. ### 1. It bounds the blast radius An agent's biggest failure mode isn't writing wrong code: it's writing plausible code in the wrong place. It optimizes locally for "make the task work" and has no instinct for what it's endangering three layers away. Ports and adapters answer that structurally. The domain has no outward dependencies; all the volatile stuff (HTTP, the database, third-party APIs, message queues) lives in adapters on the outside. So the surface where an agent *should* work (adapters) is physically separated from the surface it should almost never touch (the domain core). You're not hoping the agent respects a boundary. The boundary is load-bearing. ### 2. The core is testable in isolation, so the agent has a fast feedback loop A pure domain, free of I/O, can be tested without spinning up a database or a web server. That matters more for an agent than for you, because the agent's whole working loop is *edit → run → read the result → correct*. The faster and more focused that loop, the better the agent performs. When the business logic is a pure core, the agent can write a test, run it in milliseconds, and get an unambiguous signal: "the rule still holds" or "it doesn't." When the logic is smeared across an infrastructure-coupled class, the only way to test it is an integration test that's slow, flaky, and vague about *what* broke. A good architecture gives the agent a truth oracle. A bad one gives it a shrug. ### 3. The ports name the work in the domain's language A `PaymentGateway` port with `authorize(Payment)` and `capture(Payment)` tells an agent what the system does in business terms. A tangle of `HttpClient` calls and DTO mapping tells it nothing except how the plumbing happens to be wired this week. LLMs are exceptionally good at following well-named intent and easily lost in incidental complexity. Ports are, in effect, a curated interface an agent can reason about, the same reason [ubiquitous language](/blog/claude-code-jira-mcp-legacy-codebase) helps a human onboard. You're not just organizing code; you're handing the agent a map written in the right vocabulary. ## What it looks like in Java Here's the tangled version an agent will happily damage: ```java // OrderService.java: everything in one place public void ship(Long orderId) { Order order = jpaRepository.findById(orderId).orElseThrow(); order.setStatus(SHIPPED); order.setShippedAt(Instant.now()); // billing keys off this jpaRepository.save(order); // agent adds its feature right here, next to the timestamp it doesn't understand emailClient.send(order.getCustomerEmail(), "Your order shipped"); } ``` And the hexagonal version, where the same feature has an obvious, safe home: ```java // Domain core: pure, no framework, no I/O public final class ShipOrder { private final OrderRepository orders; // a port private final NotificationPort notifier; // a port public void handle(OrderId id) { Order order = orders.byId(id); order.markShipped(); // the shippedAt rule lives inside Order orders.save(order); notifier.orderShipped(order); // the agent extends behind this port } } // Adapter: the only file the agent needs to add public final class SlackNotificationAdapter implements NotificationPort { public void orderShipped(Order order) { /* Slack call */ } } ``` The domain didn't move. `markShipped()` still owns the timestamp rule, sealed inside the aggregate. The agent adds one adapter, implements one method, and there was never a path for it to disturb the invariant. Not because it was careful, but because the architecture never offered it the rope. ## The honest limits A thesis this tidy deserves some pushback, so let me bound it before you over-apply it. Three caveats I hold to: - **Hexagonal has a ceremony cost.** For a small script or a throwaway service, the ports-and-adapters overhead is pure friction, and an agent working in a two-file project doesn't need it. This pays off on real domains with rules worth protecting, not everywhere. - **Leaky ports leak the protection.** If your "port" is really a JPA entity or an HTTP response object in disguise, the domain isn't isolated and neither is the agent's blast radius. The architecture helps *to the exact degree that it's honest.* Half-hexagonal gives you half the guardrail. - **It reduces the blast radius; it doesn't remove it.** An agent can still write a wrong domain rule inside a perfectly pure core. Good boundaries make bad changes *contained and visible*, not impossible. You still review. You still own the output. ## The shift worth naming For twenty years we justified clean boundaries with an argument about the future: *someone will have to maintain this.* The payoff was always deferred, which is why the discipline was always the first thing dropped under deadline pressure. The AI agent collapses that timeline. The maintainer you were building for is now working in your repo this afternoon, and the code's architecture directly determines whether that agent is an accelerator or a hazard. Good structure stopped being a gift to a hypothetical future engineer. It's leverage you cash in today. So the surprising conclusion is the least surprising advice in software, with new stakes: **isolate your domain.** Not because a book said so, but because the thing now writing half your code performs exactly as well as your boundaries let it. Hexagonal architecture was always good craftsmanship. It's now also the interface through which your agents do their best work. *This connects to two things I've written before: [why architecture discipline matters more, not less, when AI writes the code](/blog/software-craftsmanship-ai-era), and [how I take a legacy Java codebase from ticket to pull request with an agent](/blog/claude-code-jira-mcp-legacy-codebase).* --- # Why I Left the AI IDE for the Terminal, and Built Tooling Around It URL: https://djamel-bougouffa.com/blog/why-i-left-the-ai-ide-for-the-terminal/ Date: 2026-07-01 Tags: AI, Software Engineering > I traded my AI-enhanced IDE for a terminal agent. It stuck, not because the terminal is cooler, but because it's composable. Here's what changed, the honest trade-offs, and the tooling I had to build. For about a year, my coding assistant lived inside my editor. Autocomplete in the gutter, a chat panel on the right, inline diffs I could accept with a keystroke. It was good. Then one afternoon I closed it, opened a terminal, and started giving the same tasks to a CLI agent instead. I expected to switch back within a week. I didn't. Months later the IDE is still there. I just don't ask it to *think* anymore. This is the honest account of why the terminal won for me, what it still loses at, and the tooling I had to build to make the switch actually pay off. ## The day I closed the panel The thing that pushed me out wasn't a feature. It was a feeling I kept having: the IDE assistant was a passenger in my editor, and the terminal agent was a colleague at my desk. Inside the IDE, the assistant mostly *suggested*. It proposed a diff, I accepted or rejected it, and the loop ended there. Anything beyond editing a file (running the test suite, checking the git history, reproducing a bug, calling an internal API) was my job. The assistant watched. In the terminal, the agent *acts*. It reads files, runs the build, parses the failure, edits, runs the tests again, and only comes back to me when it has something worth showing or a decision worth asking about. The unit of work stopped being "a diff" and became "a task." That's the difference that made it stick. Everything below is just me explaining that difference to myself. ## Why it stuck (it's not because the terminal is cooler) Three reasons, in order of how much they actually matter. ### 1. Composability This is the real one. The terminal is the one place where every tool a developer already uses speaks the same language: text in, text out. The moment the agent lives there, it can compose with all of it without anyone writing an integration. It runs `git` directly. It pipes build output into a grep. It calls my own CLIs. It talks to external systems over [MCP](/blog/model-context-protocol-mcp-cli-rust-ide). None of that required a plugin, an extension API, or a vendor's blessing. The IDE assistant, by contrast, could only do what its extension surface allowed: a walled garden, however nice the walls. An IDE is a product. The terminal is a protocol. You can build on a protocol. ### 2. Context that survives the session On a real codebase, especially a legacy one, most of the work is *understanding* before changing. The terminal agent treats the whole repository as its working set: it greps, follows call chains, reads what it needs, and keeps that context across a long session. I'm not re-explaining the project structure every few minutes. In the IDE, context was implicitly "the files I have open." Powerful for the function under my cursor, weak for "trace how this value flows through eight service layers." The terminal flipped that, and tracing flows is most of what legacy work actually is. ### 3. Control In the terminal I can see exactly what the agent is about to do, interrupt it, and steer. The loop is legible. When the agent is composing real commands rather than operating a hidden editor API, I can supervise it instead of trusting it. For anything touching production code, that legibility is worth more than convenience. ## The honest trade-off If I only told you the wins, this would be a sales pitch, not an account. The IDE still does several things better, and pretending otherwise would be dishonest. - **Visual navigation.** Jump-to-definition, find-references, the call hierarchy, a real debugger with breakpoints and a watch window: these are *better* in the IDE, full stop. When I need to *step through* code, I'm back in the editor. - **Tight, local edits.** For a small, well-scoped change in a file I'm already looking at, inline autocomplete is faster than describing the task to an agent. Not every change deserves a sentence. - **Discoverability.** The IDE shows you what's possible. The terminal assumes you know. That's a real barrier, and I'll come back to it. - **Visual artifacts.** Rendering a component, inspecting a UI, reading a flame graph: the terminal is the wrong tool, and I don't pretend it isn't. So this isn't "CLI beats IDE." I still use both. The accurate version is: **the terminal is where the agent should think; the IDE is where I still go to look.** ## What the terminal unlocked (and forced me to build) Here's the part most "I switched to the terminal" posts skip. Composability is a promise, not a feature. The terminal *lets* you compose tools; it doesn't hand them to you. Once the agent became a real actor at my desk, the gaps showed up fast, and I had to build the layer that made the switch worth it. Three problems appeared almost immediately. **The output was too noisy.** Agents read tool output as tokens, and raw developer output is mostly noise: ANSI color codes, verbose JSON, duplicated stack frames. The agent was paying, in context and in dollars, to read junk. So I built [**trimcp**](https://github.com/rustkit-ai/trimcp), an MCP proxy in Rust that sits between the agent and its tools and compresses what comes back: it strips ANSI, minifies JSON, and deduplicates repetitive blocks before the agent ever sees them. Same information, a fraction of the tokens. **The commands were too expensive.** A lot of routine dev operations produce far more output than the agent needs. This one I didn't have to build myself: [**RTK**](https://www.rtk-ai.app/) (Rust Token Killer) is an open-source CLI proxy that transparently rewrites common commands so they return only the signal. I wired it in through Claude Code hooks, so it happens with zero overhead in the prompt and nothing for me to remember. **Finding the right code was too slow.** On a large repo, an agent can waste half a session just rediscovering structure. [**semtree**](https://github.com/rustkit-ai/semtree) does on-device semantic code search (tree-sitter parsing, local embeddings, an HNSW index, no API keys) so the agent can ask "where is the logic that does X?" and get the relevant code, not a thousand grep hits. On top of that sit the boring-but-essential pieces: a per-project `CLAUDE.md` that encodes the conventions and landmines (the single highest-leverage file I maintain; [I've written about that separately](/blog/claude-code-jira-mcp-legacy-codebase)), and a handful of custom skills for the workflows I repeat. None of this is exotic. It's plumbing. But it's plumbing the IDE never let me build, because I didn't own the pipes. **That's the whole argument in one sentence: the terminal won because I could build around it.** ## Who this is for, and who it isn't I'm wary of universal advice, so let me scope it. This setup fits a **senior, autonomous engineer** who already knows the codebase well enough to supervise an agent and judge its output. If you can tell good work from plausible-looking work, the terminal multiplies you. The composability and control are leverage in experienced hands. It fits **legacy and backend work** especially well, where the job is mostly understanding, tracing, and careful change, and visual tooling matters less. It fits **less well** if you lean on visual debugging and UI work all day, or if you're early enough in your career that you can't yet catch an agent's confident mistakes. The IDE's guardrails and discoverability are genuinely valuable there, and "the terminal assumes you know" stops being a quirk and becomes a wall. ## The takeaway The framing of "AI IDE vs terminal agent" is a bit of a trap. It's not really CLI versus IDE; it's **composability versus lock-in.** I didn't move to the terminal because it's minimalist or nostalgic. I moved because it's the one environment where the agent can reach every tool I use, where I can see what it's doing, and, most importantly, where I can build the missing pieces myself. The IDE is still open in another window. I just stopped asking it to do the thinking. *trimcp and semtree, the Rust tooling I built, are part of [rustkit-ai](/work/rustkit-ai). I'll break down the token numbers behind trimcp in a follow-up.* --- # Deploying a Self-Hosted RAG App on AWS with Terraform: Why I Chose EC2 Over Bedrock URL: https://djamel-bougouffa.com/blog/deploying-rust-rag-app-aws-terraform-ec2-bedrock/ Date: 2026-05-30 Tags: DevOps, AWS, Rust > A practical breakdown of deploying a Rust RAG application on AWS EC2 using Terraform: stateful Qdrant data on EBS, GitLab OIDC for credential-free CI/CD, and the real trade-offs between self-hosting and AWS Bedrock. When I started building a Rust-based RAG system for automatically documenting legacy codebases, the first production question wasn't about the model or the embeddings. It was about where to run it. The obvious answer in 2026 is AWS Bedrock. It's managed, it scales, and AWS pushes it hard. I chose EC2 instead. This article explains why, and walks through the Terraform infrastructure I built to make it work reliably. ## Why Not Bedrock? Bedrock is the right choice for a lot of teams. But it wasn't right for me, for three reasons. **I needed Qdrant.** The application uses [Qdrant](https://qdrant.tech) as its vector database, a high-performance similarity search engine written in Rust. Bedrock's native vector store is Amazon OpenSearch Serverless or pgvector. Migrating to either would have meant rewriting core query logic and losing features I depended on: named collections, payload filtering, and fine-grained distance metrics. **I run a Rust binary, not Python.** Most Bedrock tutorials assume LangChain + Python. My backend is a compiled Rust binary. The overhead of adapting my architecture to Lambda (cold starts, binary size, async runtime) was not worth it for an internal tool with predictable traffic. **Cost predictability mattered.** Bedrock charges per token consumed. For a tool that indexes entire Java codebases (sometimes millions of tokens per project), the billing becomes unpredictable. A single EC2 `t3.medium` at ~$30/month is easier to reason about. ## The Stack Before getting into Terraform, here's what I'm deploying: - **Backend**: Rust binary packaged as a Docker image, running the RAG engine, HTTP API, and MCP server - **Frontend**: Angular app served via nginx in a second Docker container - **Vector store**: Qdrant, running as a Docker container on the same instance - **Database**: SQLite for project metadata (no RDS, which keeps costs down) - **LLM provider**: OpenRouter (calls Claude, GPT-4, etc. via a single API) Everything runs on one EC2 instance behind an Application Load Balancer. Not microservices. Not Kubernetes. One instance with Docker Compose. ## The Infrastructure Here's the full picture of what Terraform provisions: ``` Internet │ ▼ ALB (HTTPS:443 / HTTP:80→301) │ ▼ EC2 t3.medium (Amazon Linux 2023) ├── nginx container (port 3000 → frontend) ├── backend container (port 8080 → Rust API) └── qdrant container (port 6333 → vector DB) │ ▼ EBS gp3 volume (persistent /data) ├── qdrant_storage/ └── sqlite databases ``` All secrets (LLM API keys, OAuth credentials) live in AWS Secrets Manager and are injected into the EC2 instance at boot via `user_data`. Images are stored in ECR and pulled on each deployment. ## Key Terraform Decisions ### Remote State First Before `terraform apply`, you need a place to store the state. Two AWS CLI commands: ```bash aws s3 mb s3://my-app-tfstate --region eu-west-1 aws dynamodb create-table \ --table-name my-app-tflock \ --attribute-definitions AttributeName=LockID,AttributeType=S \ --key-schema AttributeName=LockID,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --region eu-west-1 ``` Then init with the backend: ```bash terraform init \ -backend-config="bucket=my-app-tfstate" \ -backend-config="key=prod/terraform.tfstate" \ -backend-config="region=eu-west-1" \ -backend-config="dynamodb_table=my-app-tflock" ``` The DynamoDB table provides state locking, which is critical if multiple engineers or CI pipelines run Terraform concurrently. ### The Stateful Problem: Qdrant on EBS This is the most important architectural decision. Qdrant stores vector collections on disk. If you let Terraform destroy and recreate the EC2 instance (which happens on AMI updates or instance type changes), you lose all indexed data. Reindexing a large codebase takes hours. The fix is a **separate EBS volume** with `prevent_destroy`: ```hcl resource "aws_ebs_volume" "data" { availability_zone = var.availability_zones[0] size = var.data_volume_size_gb type = "gp3" encrypted = true lifecycle { prevent_destroy = true } } resource "aws_volume_attachment" "data" { device_name = "/dev/xvdf" volume_id = aws_ebs_volume.data.id instance_id = aws_instance.app.id force_detach = false } ``` The `prevent_destroy = true` means Terraform will refuse to execute any plan that would delete this volume. It's a guardrail against accidental `terraform destroy`. If you genuinely need to delete it, you remove the lifecycle block, apply, then destroy. On the instance, `/data` is mounted from this volume at boot via `user_data`. SQLite databases and Qdrant collections both live there. The instance is replaceable; the data volume is not. I also set up daily AWS Backup: ```hcl resource "aws_backup_plan" "daily" { name = "${local.prefix}-backup-daily" rule { rule_name = "daily-7days" target_vault_name = aws_backup_vault.main.name schedule = "cron(0 2 * * ? *)" lifecycle { delete_after = 7 } } } ``` Seven days of daily snapshots. If something corrupts the Qdrant storage, you restore from the previous night's backup. ### Secrets Manager for LLM Keys Never put API keys in `terraform.tfvars` or environment variables in `user_data`. Use Secrets Manager: ```hcl resource "aws_secretsmanager_secret" "openrouter_api_key" { name = "${local.prefix}/openrouter-api-key" recovery_window_in_days = 7 } resource "aws_secretsmanager_secret_version" "openrouter_api_key" { secret_id = aws_secretsmanager_secret.openrouter_api_key.id secret_string = "PLACEHOLDER_SET_ME" lifecycle { ignore_changes = [secret_string] } } ``` The `ignore_changes = [secret_string]` is essential: Terraform creates the secret with a placeholder, then you set the real value via CLI. On subsequent `terraform apply` runs, Terraform won't overwrite what you set manually. ```bash aws secretsmanager put-secret-value \ --secret-id arn:aws:secretsmanager:eu-west-1:...:secret:prod/openrouter-api-key \ --secret-string "sk-or-v1-..." ``` The EC2 instance has an IAM role with permission to read these secrets. `user_data` pulls them at boot and injects them as environment variables into the Docker Compose stack. ### ALB with Automatic TLS The ALB handles TLS termination. If you provide a `domain_name` and a Route 53 `zone_id`, Terraform creates and validates an ACM certificate automatically: ```hcl resource "aws_acm_certificate" "main" { count = local.has_domain ? 1 : 0 domain_name = var.domain_name validation_method = "DNS" lifecycle { create_before_destroy = true } } ``` HTTP on port 80 redirects to HTTPS with a 301. The TLS policy is `ELBSecurityPolicy-TLS13-1-2-2021-06`, which enforces TLS 1.3 and 1.2 only. Without a domain, the stack still works: you get an HTTP-only ALB DNS endpoint. Useful for staging environments. ## CI/CD Without Long-Lived Credentials The classic approach for GitLab CI → AWS is to create an IAM user, generate access keys, and store them in CI variables. This works, but you end up with static credentials that never expire and are stored in your GitLab settings forever. The better approach is **OIDC federation**: GitLab generates a signed JWT for each pipeline run. AWS verifies the JWT against GitLab's OIDC provider and issues temporary credentials. No static keys anywhere. ```hcl resource "aws_iam_openid_connect_provider" "gitlab" { url = var.gitlab_url client_id_list = [var.gitlab_url] thumbprint_list = [var.gitlab_oidc_thumbprint] } resource "aws_iam_role" "gitlab_ci" { name = "${local.prefix}-gitlab-ci" assume_role_policy = data.aws_iam_policy_document.gitlab_ci_assume.json max_session_duration = 3600 } ``` The trust policy restricts which pipelines can assume the role: only `main` branch and tags of the specific project: ```hcl condition { test = "StringLike" variable = ":sub" values = [ "project_path:/:ref_type:branch:ref:main", "project_path:/:ref_type:tag:ref:*", ] } ``` In `.gitlab-ci.yml`, the job requests an OIDC token and exchanges it for AWS credentials: ```yaml deploy: id_tokens: AWS_OIDC_TOKEN: aud: https:// script: - > export $(aws sts assume-role-with-web-identity --role-arn $OIDC_ROLE_ARN --web-identity-token $AWS_OIDC_TOKEN --role-session-name gitlab-ci --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]' --output text | awk '{print "AWS_ACCESS_KEY_ID="$1"\nAWS_SECRET_ACCESS_KEY="$2"\nAWS_SESSION_TOKEN="$3}') - docker build -t $ECR_BACKEND_IMAGE . - docker push $ECR_BACKEND_IMAGE - aws ssm send-command --instance-ids $INSTANCE_ID --document-name AWS-RunShellScript --parameters 'commands=["cd /app && docker compose pull && docker compose up -d"]' ``` The CI role only has permission to push to ECR and trigger SSM commands on the EC2 instance. Nothing else. ## What I Would Do Differently **Use ECS Fargate for the application containers, keep EC2 for Qdrant.** The application containers (backend, frontend) are stateless: they're perfect for Fargate. The only reason I kept everything on EC2 was operational simplicity at the start. With Fargate for the app and a dedicated EC2 instance for Qdrant, you get rolling deployments and better fault isolation. **Use RDS PostgreSQL instead of SQLite for metadata.** SQLite works fine for a single instance, but it creates problems the moment you want to scale horizontally or run blue/green deployments. The switch cost is low; the future flexibility gain is high. **Set up CloudWatch alarms from day one.** I added monitoring after the first production incident. The ALB unhealthy host count alarm should be the first thing you create, not the last. ## Conclusion Choosing EC2 over Bedrock wasn't ideological. It was a pragmatic response to three constraints: Qdrant as the vector store, a compiled Rust binary, and cost predictability for a variable indexing workload. The Terraform infrastructure described here (roughly 500 lines across eight files) provisions a production-grade deployment with TLS, daily backups, credential-free CI/CD, and protected persistent storage. It's not the simplest setup, but each piece earns its place. If you're running a self-hosted AI application and have similar constraints, the EC2 + EBS + Secrets Manager pattern is worth considering. The operational burden is real, but the control and cost profile often justify it. The full Terraform template from this article, generalized and ready to fork, is available at [github.com/Strawbang/terraform-aws-ai-stack](https://github.com/Strawbang/terraform-aws-ai-stack). --- # How Standardizing a Tech Stack Cut Product Creation Time by 80% URL: https://djamel-bougouffa.com/blog/stack-standardization-80-percent-faster/ Date: 2026-05-05 Tags: Software Engineering, Tech Leadership > At METRO France, 93 warehouses, multiple product teams, and a fragmented codebase were slowing everything down. Here's how aligning on a shared tech stack transformed delivery velocity, and what every engineering leader should know before attempting it. When I joined the product team at METRO France as a consultant via Wemanity Group, the engineering organization had a problem that wasn't obvious from the outside: each product was built in isolation. Different teams had made different technology choices. Some products used one framework, others another. There was no shared component library, no common API conventions, no unified deployment strategy. Engineers couldn't move between products without a significant ramp-up period. Every new feature required reinventing decisions that had already been made elsewhere. The result was predictable: slow onboarding, duplicated effort, fragile deployments, and a growing sense that the engineering organization couldn't keep up with the business. This is the story of how we fixed it, and what I learned about the real cost of technical fragmentation. ## The Problem with "Good Enough" Diversity Before the standardization effort, each product team had made independent technology choices: different frameworks, different databases, different CI pipelines. Every column was a silo. Engineers couldn't move between them. Nothing was reusable. There's a seductive argument for letting teams choose their own tools: autonomy enables creativity, and each team knows its problem best. It's not entirely wrong. But at a certain scale, that autonomy becomes a tax. At METRO France, the tax showed up in three ways: **Onboarding latency.** A new engineer joining any team couldn't contribute for weeks because they had to learn a product-specific stack from scratch. There was no foundational knowledge that transferred. **No reuse.** A component built by one team to solve a UX problem couldn't be used by another team, even when the problem was identical. Every team reinvented the same solutions. **Deployment fragility.** Without shared infrastructure patterns, each product had its own deployment quirks. A fix in one pipeline didn't help the others. Incidents were isolated but frequent. The business impact was clear: building a new product took months. Iterating on an existing one took longer than it should. Technical debt was accumulating silently across every codebase. ## Choosing a Standard That Actually Sticks The first decision, and the most politically fraught one, was choosing the stack. We landed on **TypeScript, React, Node.js, TypeORM, and GraphQL** for application development, with **GCP and Kubernetes** for infrastructure, and **GitHub Actions** for CI/CD. This wasn't an arbitrary choice. Each technology was selected for a specific reason: - **TypeScript** over plain JavaScript: type safety across codebases, better IDE support, and a shared contract between front-end and back-end teams. - **React** for the front-end: large ecosystem, component reuse, and a talent pool deep enough to hire against. - **Node.js + GraphQL** for the API layer: unified query interface, strong typing with code generation, and the ability to aggregate data sources without over-fetching. - **GCP + Kubernetes**: already partially in use, well-documented, and scalable to the 93-warehouse operational footprint. The key insight: **a standard only sticks if it's better for the teams, not just better on paper.** We chose technologies that were genuinely productive to work with, not the most fashionable or the most theoretically correct. The shared base configuration made the standard concrete from day one: ```json // tsconfig.base.json (inherited by all products) { "compilerOptions": { "target": "ES2022", "strict": true, "noImplicitAny": true, "noUncheckedIndexedAccess": true, "esModuleInterop": true, "moduleResolution": "bundler" } } ``` Each product's `tsconfig.json` simply extended this base. One decision, made once, consistently enforced everywhere. ## Implementation: Adoption Before Enforcement The biggest mistake organizations make when standardizing is mandating compliance before demonstrating value. We took the opposite approach. Before asking any team to change their stack, we built a reference implementation: a small, real product using the new standard, shipped to production. It had to work, not just look good in a demo. Once the reference existed, adoption spread faster than we expected. Engineers could see the patterns in action. They could ask questions with real examples in hand. The new standard wasn't a policy document. It was working code. From there, we introduced shared tooling: a design system with reusable React components, shared TypeScript configs, ESLint rules that encoded architectural decisions, and a common CI/CD pipeline template that teams could copy and adapt. New products were started on the new stack by default. Existing products were migrated incrementally, not all at once to avoid disruption, but module by module, as features were added or refactored. The shared CI/CD template removed weeks of infrastructure setup per project: ```yaml # .github/workflows/ci.yml (reusable across all products) name: CI on: [push, pull_request] jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 cache: 'npm' - run: npm ci - run: npm run lint - run: npm run build - run: npm test - uses: ./.github/actions/deploy-gcp if: github.ref == 'refs/heads/main' ``` After standardization, the same four columns looked like this: ```mermaid flowchart TB PA[Product A] & PB[Product B] & PC[Product C] & PD[Product D] --> SF SF["Shared Foundation\nReact + TypeScript · Node.js + GraphQL\nGCP / Kubernetes · GitHub Actions"] ``` Any engineer could contribute to any product from day one. ## The Bonus Management Product: A Case Study Within the Case Study The most concrete example of what this standardization enabled was the bonus management product. METRO France's 93 warehouses had been managing employee bonuses through spreadsheets. Store managers filled in data manually, HR consolidated it with significant lag, and errors were frequent. The process was time-consuming, unauditable, and a recurring source of employee dissatisfaction. Building this product in the old world would have required months of setup, custom tooling, and a team that couldn't be reused elsewhere afterward. In the new world, we built it on the standardized stack from day one. We reused shared components for the UI. The GraphQL API followed established patterns. The deployment pipeline was templated. The team that built it had already worked on other products in the new stack and hit the ground running. The product launched across all 93 warehouses nationally. Bonus calculations moved from spreadsheets to an auditable digital system. Store managers reported faster processing. HR freed up significant time. Employee satisfaction scores in warehouses improved measurably. But beyond the product itself, the most significant outcome was what it demonstrated: **the standardized stack had become a multiplier**. Building this product took a fraction of the time it would have taken before. ## The 80% Number The 80% acceleration in product creation cycles that we measured wasn't magic: it was the sum of several compounding effects: ```mermaid flowchart LR A[No stack decision overhead] --> R[80% faster delivery] B[Shared components –40-60% UI work] --> R C[Template CI/CD pipelines] --> R D[Cross-team knowledge transfer] --> R E[Fewer incidents] --> R ``` 1. **No stack decision overhead.** Teams started building immediately, not deliberating over technology choices. 2. **Shared components reduced UI work** by 40–60% on new products. 3. **Template CI/CD pipelines** eliminated weeks of infrastructure setup per project. 4. **Cross-team knowledge transfer** meant engineers could contribute to any product from day one. 5. **Fewer incidents** from better-understood, better-documented infrastructure patterns. Each of these alone would have been meaningful. Together, they transformed how fast the organization could ship. ## What Engineering Leaders Should Know If you're considering a standardization effort in your organization, here's what I'd pass on from this experience: **Start with the pain.** The case for standardization needs to be grounded in real problems, not theoretical elegance. Show the onboarding cost. Show the duplicated effort. Show the deployment incidents. Make the current state's cost legible. **Build before you mandate.** A reference implementation is worth ten strategy documents. Ship something real, then show people why the new way is better. **Standardize the decisions, not the tools.** The goal isn't uniformity for its own sake: it's reducing decision fatigue and enabling reuse. A standard TypeScript config is more valuable than a standard framework, because it removes a low-level decision without constraining higher-level architecture. **Expect gradual migration.** Rewriting everything at once is a way to break things and burn engineers out. Migrate incrementally, product by product, module by module. New projects adopt the standard by default; existing ones migrate as they evolve. **Measure the outcome, not the process.** The metric that matters is delivery velocity, not "percentage of products on the new stack." Track what you actually care about. --- Standardizing a tech stack is, at its core, a bet that shared infrastructure creates more value than individual autonomy. At METRO France, that bet paid off clearly: faster delivery, better products, and an engineering team that could work together rather than in parallel silos. The 80% figure is real, but the more important thing it represents is this: when engineers stop reinventing the same decisions over and over, they can spend that time building things that actually matter to users. That, more than any specific technology, is the point. --- # AI Broke Our Production Code. Here's the Craftsmanship Rule That Would Have Prevented It URL: https://djamel-bougouffa.com/blog/software-craftsmanship-ai-era/ Date: 2026-05-04 Tags: Software Engineering > An AI agent silently broke a 10-year-old business rule on a legacy Java codebase. No test caught it. It surfaced in production. Here's what that taught me about Software Craftsmanship, and why these principles matter more than ever when AI is writing your code. When tools that generate code at scale become mainstream, it's tempting to assume the old rules no longer apply. Why obsess over naming when an AI can understand any variable name? Why write tests when the AI will rewrite the function anyway? Why refactor when generating a new implementation is faster than cleaning up the old one? That reasoning is dangerous. And in my experience, the codebases that suffer most from AI-assisted development are the ones that abandoned discipline first. ## What Software Craftsmanship Actually Is Software Craftsmanship, formalized in the [2009 manifesto](https://manifesto.softwarecraftsmanship.org/), is a set of professional standards: clean code, test-driven development, continuous refactoring, simple design, and collective ownership. It treats code as a craft: something you take pride in, something you maintain. It's not about perfection. It's about professional responsibility. ## The Principles That Survived After working with AI tools on production codebases for over a year, here's what I've found: the craftsmanship principles don't disappear. They become load-bearing. ### 1. Tests Are Your Only Safety Net This was true before AI. It's critical now. AI generates code quickly but without deep understanding of intent. It can produce code that passes manual review and silently breaks a business rule that was never written down. Tests are the only systematic way to catch this. I've seen it directly. Working on a legacy Java codebase with no test coverage, an AI agent confidently refactored a calculation that had been running for years. The change looked correct, passed the linter, and introduced a silent regression on a business rule nobody had documented. There was no test to catch it. It surfaced in production. That session was the moment I stopped treating craftsmanship as optional when working with AI. I started treating it as the precondition. TDD in particular changes the dynamic: writing the test first forces you to define what "correct" means before the AI proposes an implementation. The AI then has a target to hit. Without that target, you're reviewing code hoping it's right. That's not engineering, it's optimism. I've stopped treating test coverage as a metric. I treat it as a precondition. If the AI makes a change that isn't covered by tests, either the tests come first or the change doesn't happen. Some in the craftsmanship community argue that TDD doesn't apply the same way when AI is writing the code, and they're right that the *mechanics* change. The classic Red → Green → Refactor cycle was designed for a human writing both the test and the implementation, where the act of coding is itself a design activity. With AI, that part changes. But the contract the test creates doesn't. The test written before the implementation still defines what "correct" means. It still forces you to think through the expected behavior before generating anything. The AI writes the Green, but you own the Red. And the Refactor is entirely yours. What disappears is the design-through-implementation feedback loop. What remains, and what matters most on legacy codebases with AI, is the safety contract. Concretely, before asking an AI to touch any business logic, I write a characterization test first: ```java // Step 1: write the test that defines what "correct" means @Test void shouldApplyReducedVatRateForB2bGermanOrders() { Order order = Order.builder() .amount(new BigDecimal("1000")) .customerType(CustomerType.B2B) .country("DE") .build(); BigDecimal vat = vatService.calculateVat(order); // Germany B2B: 19%, not the standard 20% assertThat(vat).isEqualByComparingTo(new BigDecimal("190")); } // Step 2: now the AI has a target, and a safety net ``` Without that test, the AI would have refactored `calculateVat()` to use a constant `VAT_RATE = 0.20`: clean, consistent, and silently wrong for every B2B German order. ### 2. Readability Is for the Next Human AI can parse any code. Humans can't. The argument "the AI understands it, so naming doesn't matter" confuses the tool with the process. When a bug surfaces at 2am, it's not the AI that's paged. When a new developer joins the team, they're reading the code in a PR review, not in a chat window. When your tech lead questions an architecture choice, they need to understand the intent, not just the mechanics. Clean code is documentation that never goes stale. In AI-assisted development, where the volume of generated code increases, readability becomes a more valuable property, not less. A variable named `result` is technical debt. A variable named `eligibleInvoicesForRecovery` is self-documenting. The AI doesn't care. Your team does. ```java // What AI often generates: correct, unreadable BigDecimal r = o.getA().multiply(getR(o.getC(), o.getT())); // What craftsmanship demands BigDecimal vatAmount = order.getSubtotal() .multiply(vatRateResolver.resolve(order.getCountry(), order.getCustomerType())); ``` The second version doesn't just look better. When the VAT bug surfaces at 2am, your colleague finds the right class in 10 seconds instead of 10 minutes. ### 3. Small Changes, Reversible by Default AI tools love generating large changes. A single prompt can touch 20 files, rename 3 abstractions, and restructure 2 modules. This is genuinely useful, and genuinely dangerous. The craftsmanship principle here is: *keep changes small and reversible*. One concept per commit. Tests before the refactor. A diff that a colleague can review in 15 minutes without losing context. This is also where the Boy Scout Rule earns its keep: *leave the code a little better than you found it*. Not completely redesigned. Not fully modernized. Just a little cleaner. That discipline prevents the "AI did a big refactor while fixing a bug" problem that turns a routine PR into a 3-hour review session. ```bash # What AI tends to produce: one massive, hard-to-review commit git log --oneline # a3f9c12 refactor order processing # (touches: OrderService, VatCalculator, InvoiceGenerator, # PaymentProcessor, OrderRepository, OrderMapper: 847 lines changed) # What craftsmanship looks like # 1f2a3b4 PROJ-421: extract VatRateResolver from OrderService # 8c9d0e1 PROJ-421: add B2B VAT rate for DE, AT, CH # 2e3f4a5 PROJ-421: cover VatRateResolver with unit tests # 7b8c9d0 PROJ-421: fix incorrect VAT rate on B2B German orders ``` The second history is reviewable, bisectable, and revertable. The first is a gamble. ### 4. Refactoring Is a Continuous Practice, Not a Sprint AI-generated code tends toward duplication. Ask an AI to implement a feature, and it will often create a new version of something that already exists nearby. It's not wrong. It's responding to the immediate problem without knowing the full codebase. The discipline is in the refactoring step that follows. Before merging, ask: does this duplicate something? Does this belong here? Is there a better abstraction? This isn't the AI's job. It's yours. AI is good at executing. It's not good at knowing when something is already done correctly somewhere else in a 200k-line codebase. That's institutional knowledge. That's craftsmanship. ```java // AI generated this in OrderService: looks fine in isolation private BigDecimal applyDiscount(BigDecimal amount, String customerType) { if ("B2B".equals(customerType)) return amount.multiply(new BigDecimal("0.90")); return amount; } // But this already existed in PricingService, for 3 years public BigDecimal applyCustomerDiscount(BigDecimal basePrice, CustomerType type) { return basePrice.multiply(discountConfig.getRateFor(type)); } ``` Two implementations of the same logic, diverging silently. The AI didn't know. You do, if you look. ### 5. Collective Ownership Requires Explicit Standards The most underrated principle in the craftsmanship manifesto is collective code ownership: the idea that the whole team is responsible for the whole codebase, not just "their" modules. AI tools make this harder by default. Everyone uses their own prompts, their own model, their own style. Without explicit shared standards, AI-assisted development accelerates divergence: different naming conventions, different test styles, different levels of abstraction in the same service. The fix is explicit: a `CLAUDE.md`, a `CONTRIBUTING.md`, linting rules with no exceptions, PR templates that enforce the review checklist. These aren't bureaucracy. They're the scaffolding that makes collective ownership possible when part of the team is an AI. ```markdown # CLAUDE.md (extract) ## Non-negotiable rules - Never modify `LegacyOrderMapper` without characterization tests first - All monetary values in **cents (Long)**, never BigDecimal in the DB layer - VAT rates live in `VatRateConfig`: never hardcode them - Do NOT add `@Transactional` to scheduler jobs: they hold connections for minutes ## Before any refactoring 1. Write tests that describe current behavior 2. Confirm tests pass on the unmodified code 3. Refactor 4. Confirm tests still pass ``` This file is the shared contract. Every developer reads it. The AI agent reads it. Standards stop being individual habits and become team infrastructure. But `CLAUDE.md` is a *soft* constraint: the AI reads it if you configure it to. Linting rules are *hard* constraints: the CI breaks if anyone, human or AI, violates them. For hexagonal architecture specifically, `eslint-plugin-boundaries` enforces layer isolation at the import level: ```js // .eslintrc: architectural boundaries as linting rules "boundaries/element-types": ["error", { default: "disallow", rules: [ // Domain is pure: no external imports allowed { from: "domain", allow: [] }, // Application layer can only depend on domain { from: "application", allow: ["domain"] }, // Only adapters can touch infrastructure { from: "adapters", allow: ["application", "domain"] }, ] }] ``` If the AI generates `import { PrismaClient } from '@prisma/client'` inside your domain, the pipeline fails immediately, before review, before merge, before production. The architecture enforces itself. For Java, you have three options depending on how far you want to go: **ArchUnit**: enforced as a test assertion, useful on legacy codebases where restructuring isn't an option: ```java @Test void domainShouldNotDependOnInfrastructure() { noClasses().that().resideInAPackage("..domain..") .should().dependOnClassesThat() .resideInAPackage("..infrastructure..") .check(importedClasses); } ``` **Multi-module Maven/Gradle**: structural enforcement. If `domain` doesn't declare `infrastructure` as a dependency, the import physically can't compile: ```xml ``` **JPMS (Java 9+)**: the most native option. `module-info.java` declares exactly what each module can see. No `requires infrastructure` means a compile error, full stop: ```java // domain/src/main/java/module-info.java module com.example.domain { exports com.example.domain.model; exports com.example.domain.port; // no 'requires infrastructure': import is a compile error } ``` On a greenfield project, multi-module or JPMS is the right answer. ArchUnit shines on legacy codebases where you can't restructure yet but need the guardrail immediately. This is the difference between *guidelines* and *governance*. `CLAUDE.md` tells the AI what to do. The linter, or the compiler, makes it impossible to do otherwise. ## What AI Actually Changes To be fair: AI does change some things. **Exploration is faster.** Understanding a new codebase, tracing a data flow, finding where a bug lives. AI compresses this from hours to minutes. This is genuinely good. The archaeology part of software work is the least craft-intensive part anyway. **Boilerplate is gone.** Writing a CRUD endpoint, a DTO, a mapper: these things no longer require craft. They require a clear spec. That's a net positive: it shifts focus to where judgment actually matters. **The craft is in the constraints.** The skill is no longer in typing. It's in defining what is correct before generating. The specification, the test, the `CLAUDE.md`, the review: these are the craft surfaces. They've always been the most important parts. AI just makes it obvious. **The risk multiplies without discipline.** An undisciplined developer writes messy code slowly. An undisciplined developer with AI writes messy code fast. The volume of output increases, but so does the surface area for silent regressions, hidden coupling, and business rules broken by a confident model that had no test to tell it otherwise. ## The Responsibility Stays Yours Here's what I've come to: AI doesn't reduce the responsibility of a senior developer. It moves it upstream. Before: write the code, review the code, test the code. With AI: define what correct means, constrain the generation, validate the output. The second loop requires just as much discipline. Possibly more. Because the output volume is higher, the review surface is wider, and the cognitive shortcuts are easier to take. Software Craftsmanship is the discipline that keeps you honest in that loop. The principles didn't get replaced. They became the job. And if you're leading a team, your responsibility is double: not just practicing these standards yourself, but making them the baseline everyone, human and AI, operates from. ## Key Takeaways - **Tests are a precondition, not a metric.** If the AI makes a change without test coverage, either tests come first or the change doesn't happen. - **Readability is for humans, not models.** AI can parse anything. Your team can't. Clean code is documentation that never goes stale. - **Keep changes small and reversible.** AI generates large diffs by default. Your discipline is in constraining it: one concept per commit, reviewable in 15 minutes. - **Refactoring is your job, not the AI's.** AI doesn't know what already exists in a 200k-line codebase. You do. - **Explicit standards enable collective ownership.** Without a `CLAUDE.md`, a `CONTRIBUTING.md`, and enforced linting, AI-assisted development accelerates divergence across your team. - **The risk multiplies without discipline.** An undisciplined team with AI doesn't write bad code slowly: it writes bad code fast. --- # How I Use Claude Code and Jira MCP to Modernize Legacy Codebases URL: https://djamel-bougouffa.com/blog/claude-code-jira-mcp-legacy-codebase/ Date: 2026-04-28 Tags: AI, Software Engineering > How I connect Claude Code to Jira via MCP to go from ticket to pull request on legacy Java codebases: my exact setup, CLAUDE.md config, and real-world results. Legacy code is the skeleton of every successful company. It runs billing, processes orders, and holds business rules that nobody has fully understood in years. It's not going anywhere, and that's exactly why modernizing it matters. For the past several months, I've been using **Claude Code** alongside the **Jira MCP** and a carefully crafted `CLAUDE.md` to navigate and implement changes on legacy Java codebases. This article is a practical breakdown of that workflow: what it looks like, why it works, and where it still fails. ![Workflow diagram: Jira MCP and CLAUDE.md feed into Claude Code, which navigates the legacy codebase and outputs a pull request](/assets/blog/claude-code-jira-mcp-workflow.svg) ## The Problem with Legacy Codebases Before diving into the setup, it's worth naming the actual problem. Legacy code isn't hard because it's old. It's hard because of everything that surrounds it: - **No documentation** (or worse, documentation that lies) - **Implicit business rules** buried in conditionals nobody dares touch - **Missing tests** that would tell you if your change breaks something - **Original authors gone**, their knowledge left with them - **Fear**, the real killer of velocity on legacy projects The traditional approach is to read the code, ask around, and make cautious changes. It works but it's slow. An AI agent doesn't solve all of this, but it dramatically reduces the time spent on the archaeology part. ## The Stack My setup has three components: ### 1. Claude Code [Claude Code](https://claude.ai/code) is Anthropic's agentic coding tool. It runs in your terminal, has access to your filesystem, can execute commands, and iterates on its own. Unlike a chat interface, it doesn't just suggest code it reads files, makes changes, runs tests, and fixes errors. What makes it powerful for legacy work is that it can navigate a large codebase without me directing every step. I give it a task and context, and it figures out which files to read. ### 2. CLAUDE.md The Project Handbook Every project gets a `CLAUDE.md` at the root. This file is Claude Code's onboarding document. It tells the agent what it needs to know before touching anything. Here's the kind of content I put in a `CLAUDE.md` for a typical enterprise Java project: ```markdown ## Overview Maven multi-module Spring Boot application, Java 17. Two databases: PostgreSQL (application data) + Oracle (legacy ERP, read-only). All services run in Docker locally. ## Daily commands ### Environment docker compose up -d # start all containers docker compose logs -f api # follow API logs docker compose down # stop all containers ### Build & run ./mvnw clean install -DskipTests # full build ./mvnw -pl api spring-boot:run # run API module only ./mvnw test -pl api # run unit tests for api module ### Database # PostgreSQL localhost:5432, db: appdb, user: appuser ./scripts/db-reset.sh # drop + recreate + seed local DB ./scripts/db-load-dump.sh prod # load anonymized prod dump (slow, ~5min) # Oracle (legacy ERP) read-only, localhost:1521 # Never write directly use the dedicated OracleService wrapper ## Architecture - api/ REST controllers + business logic - domain/ Entities, repositories (Spring Data JPA) - integration/ Adapters to Oracle ERP (polling via Apache Camel) - scheduler/ Batch jobs (Spring Batch) - common/ DTOs, utils, shared constants ## Known landmines - Oracle ERP is read-only never attempt a write, it will corrupt downstream data - OrderService.createOrder() is NOT idempotent always check for duplicates first - LegacyCodeMapper uses reflection, adding new fields breaks silently without tests - Do NOT add @Transactional to scheduler jobs they hold connections for minutes ## Git Branches: main (prod), develop (integration), PROJ-XXXX-short-description (feature) Commits: always prefix with ticket "PROJ-123: add VAT calculation for EU orders" Push: --force-with-lease only, never --force ``` This file is the difference between Claude Code making a confident, correct change and blindly breaking something. The more specific you are, the better the results. The **Known landmines** section is the most valuable part. These are the things that would take a new developer weeks to discover. Writing them down once saves everyone including the agent from painful regressions. ### 3. Jira MCP The [Jira MCP](https://github.com/sooperset/mcp-atlassian) exposes your Jira project as a set of tools that Claude Code can call. In practice, this means Claude Code can: - Read the ticket description, acceptance criteria, and comments - Understand the full context before writing a single line - Reference related tickets and sub-tasks - Post a comment back with a summary of what was done But the most powerful thing I've built on top of this is a **custom skill in `CLAUDE.md`**: ```markdown ## Available skills | Command | Description | |----------------------------|----------------------------------------------------------| | `/test-ticket ` | Run Playwright acceptance tests for a Jira ticket | | `/mr-push [app\|connector]`| Amend + rebase + force-push with lease | | `/mr-comment [reply\|post]`| Read and reply to GitLab MR comments | | `/build [app\|connector]` | Build the app or connector | | `/sonar [app\|connector]` | Run local SonarQube analysis | ``` The `/test-ticket` skill is the one that changed how I work. I give it a ticket reference, Claude Code reads the acceptance criteria from Jira, maps them to the right Playwright test files, runs them, and reports back. The full feedback loop write code, test against the actual ticket without leaving the terminal. **Honest caveat though**: it burns a significant number of tokens per run. Reading the ticket, navigating the test files, running Playwright, interpreting results it all adds up. On a complex ticket with multiple acceptance criteria, a single `/test-ticket` run can cost more than the rest of the session combined. I still use it, but selectively not as a default step on every change. ## The Workflow: From Ticket to Implementation Here's how a typical session looks. ### Step 1 Kick off with a ticket reference I open a terminal in the project root and start Claude Code with a simple prompt: ``` Read JIRA ticket PROJ-4821 and implement the requested change. Use CLAUDE.md for project context before touching any files. ``` That's it. Claude Code reads `CLAUDE.md` first, then calls the Jira MCP to fetch the ticket, reads the description and acceptance criteria, and starts exploring the codebase. ### Step 2 Claude Code explores the codebase Based on the ticket content, the agent searches for relevant files. For a ticket like *"Fix incorrect VAT calculation for B2B orders in Germany"*, it will: 1. Search for `VAT`, `tax`, `germany`, `B2B` across the codebase 2. Read the relevant service and model files 3. Follow the call chain to understand what feeds the calculation 4. Check if there are existing tests for that flow This is the archaeology phase. Doing it manually takes 30-60 minutes on an unfamiliar codebase. Claude Code does it in 2-3 minutes. ### Step 3 Implementation with guardrails Claude Code proposes changes based on what it found. Because `CLAUDE.md` establishes the constraints (no Lombok, amounts in cents, don't touch `legacy_` tables), the changes respect the project's existing conventions. If the agent is uncertain, it asks. If it finds something suspicious in the code that might be related to the ticket, it surfaces it. ### Step 4 Review and iterate I review the diff, run the tests, and iterate. Claude Code doesn't replace the review it compresses the time spent before the review. ## What Actually Works Well After several months of using this workflow, here's what consistently delivers value: **Understanding data flows:** tracing how a value gets from the database to the API response through 8 service layers is tedious. Claude Code handles it well. **Writing tests for untested code:** this is underrated. Before making a change, I ask Claude Code to write characterization tests for the relevant code. It reads the existing behavior and writes tests that describe it. These tests catch regressions during the change. **Surfacing related code:** the agent often finds code that's related to the ticket but wasn't mentioned in it. Duplicate logic, dead code, a similar function in another module. This knowledge compounds over time. **Translating ticket language to code:** business stakeholders write tickets in business language. Developers have to mentally translate. Claude Code bridges that gap more reliably than I expected. ## What Doesn't Work Yet Honesty matters here. This workflow isn't magic. **Implicit business rules:** if the rule isn't written down (in `CLAUDE.md`, comments, or tests), Claude Code won't infer it from code patterns. A legacy codebase full of silent assumptions is still full of silent assumptions. **Highly coupled modules:** when everything depends on everything, the agent struggles to make a scoped change without touching too much. The fix there is architecture, not prompting. **Token costs on automated test loops:** automating the full test-against-ticket cycle is technically impressive but expensive. The more context the agent needs to load (ticket + test files + Playwright output), the more tokens it burns. Some automations that look great on paper don't survive contact with a real monthly bill. **100% autonomous operation:** Claude Code is a senior junior developer. It's fast, thorough, and follows instructions well. But it still needs a human to define what "correct" means and to validate the output. Treating it as a fully autonomous agent leads to bad outcomes. **Cross-cutting concerns:** security, performance, observability. These require judgment that goes beyond implementing what the ticket says. I always review for these separately. ## The CLAUDE.md Is the Real Unlock If I had to name the single most impactful part of this setup, it's the `CLAUDE.md` not the model, not the tools. A good `CLAUDE.md` captures institutional knowledge that would otherwise live only in the heads of the people who've worked on the project longest. Writing it forces you to articulate things that are usually implicit. And once it exists, any developer (human or AI) onboards faster. I update it every time I discover something that surprised me during a session. Over time, it becomes the living documentation the project never had. ## Conclusion The combination of Claude Code, Jira MCP, and a well-maintained `CLAUDE.md` doesn't eliminate the hard parts of legacy modernization. It compresses the time spent on the mechanical parts reading code, tracing flows, writing boilerplate, so that more of your time goes to judgment, architecture, and actual problem-solving. That shift is worth more than any individual feature the tools provide. If you're working on a legacy codebase and haven't tried this workflow, start with the `CLAUDE.md`. Even without Claude Code, forcing yourself to write down what the project needs newcomers to know is the most valuable 30 minutes you'll spend this week. --- # Model Context Protocol (MCP): Exposing a Rust CLI to Your IDE URL: https://djamel-bougouffa.com/blog/model-context-protocol-mcp-cli-rust-ide/ Date: 2026-03-11 Tags: AI, Software Engineering > A practical deep-dive on using MCP to expose a Rust CLI to an AI agent in the IDE: architecture, best practices, and TypeScript implementation. **Quick answer:** The Model Context Protocol (MCP) lets an AI agent inside your IDE call your own tools directly, with no IDE-specific plugin. You keep your Rust CLI as the engine, wrap it in a small TypeScript MCP server that declares its capabilities, and any MCP-compatible host (Claude Code, Cursor, Windsurf) can then run real actions on your codebase instead of just suggesting them. The **Model Context Protocol (MCP)** allows you to expose internal tools to an AI agent directly in your IDE, without IDE-specific plugins, without ad hoc integrations. Instead of giving suggestions, the agent can execute real actions on your codebase. ## The problem MCP solves AI agents embedded in IDEs are great at understanding context: analyzing code, explaining errors, suggesting refactors. But when it comes to executing real actions (running internal scripts, triggering business logic, validating outputs against proprietary rules), a friction loop appears: the agent proposes → you run it elsewhere → you paste the output back in. Every time. MCP closes this gap by providing a **standard to expose capabilities** (CLIs, APIs, internal services) via a structured contract that any compatible IDE or AI assistant can directly consume. ## MCP vs REST: not just another API The difference isn't in the message format, it's in the integration model: - **REST** exposes endpoints. The orchestration is written client-side, the client is known in advance, nothing standardizes usage by an agent. - **MCP** exposes declared capabilities (`tools`, `resources`, `prompts`). The host discovers them dynamically, the agent decides how to chain them, responses are structured to be interpretable and re-plannable. REST exposes services. MCP exposes actions exploitable by agents. ## Architecture: motor / façade / cockpit In our implementation at Ippon: - **Rust CLI** = the motor: business logic, performance, source of truth. Nothing changes here. - **TypeScript MCP server** = the façade: the contract, parameter validation, capability exposure via the MCP SDK. - **IDE/agent** = the cockpit: orchestration and UX. A single integration target replaces N IDE-specific plugins. Exposing a tool is surprisingly concise: ```typescript server.tool("analyze_module", { inputSchema: { type: "object", properties: { module: { type: "string" } }, required: ["module"] } }, async ({ module }) => { const result = await runCli(["analyze", module]); return JSON.parse(result); }); ``` ## Why not a VSCode plugin? The "smart plugin" approach hits structural problems fast: the N-IDE effect (every host is a new target), duplicated business logic that drifts from the CLI, fragile stdout parsing that turns logs into an implicit API, and IDE lock-in. MCP inverts the posture: start from the capabilities, not the interface. ## 3 design principles for agent-ready tools 1. **Decision-oriented outputs:** don't dump state, return what the agent needs to act next. 2. **Actionable errors:** structured `code + message + recoveryHint` so the agent can self-correct without human intervention. 3. **Atomic and composable tools:** `analyze_module()` → `generate_patch()` → `apply_patch()` → `run_tests()`. One tool, one intent. The agent orchestrates. ## Making a CLI agent-ready (3 changes) 1. **JSON-first stdout:** stable machine-readable output; human logs go to stderr. 2. **Separated logs:** don't mix result and diagnostic output. 3. **Categorized errors:** error code + message + recovery hint. ## Frequently asked questions ### What is the Model Context Protocol (MCP)? MCP is an open standard that lets an AI agent discover and call external capabilities (CLIs, APIs, internal services) through a structured contract. The host discovers the declared tools dynamically, and the agent decides how to chain them, so the same tool works across any MCP-compatible IDE or assistant. ### How is MCP different from a REST API? REST exposes endpoints and leaves orchestration to a client that is known in advance. MCP exposes declared capabilities (`tools`, `resources`, `prompts`) that a host discovers at runtime, and returns structured responses an agent can interpret and re-plan around. REST exposes services; MCP exposes actions an agent can use. ### Do I need to rewrite my Rust CLI to make it agent-ready? No rewrite, three adjustments. Emit machine-readable JSON on stdout and send human logs to stderr, keep result and diagnostic output separated, and return categorized errors (code, message, recovery hint) so the agent can self-correct without a human in the loop. --- This article was first published, in French, on the [Ippon Technologies blog](https://blog.ippon.fr/2026/03/11/model-context-protocol-mcp-cli-rust-ide/). --- # Spec-Driven Development: From Specification to Code with AI URL: https://djamel-bougouffa.com/blog/spec-driven-development/ Date: 2026-02-18 Tags: AI, Software Engineering > Discover Spec-Driven Development (SDD) and how AI, using Kiro, transforms a specification into structured, test-verified code. This article was co-written and published on the [Ippon Technologies blog](https://blog.ippon.fr/2026/02/18/spec-driven-development/) (in French). **Spec-Driven Development (SDD)** proposes a simple yet radical shift: the specification becomes the source of truth for the project. Before writing any code, you precisely describe what the system must guarantee. These guarantees directly guide the architecture, implementation, and tests. ## The core idea SDD is a development approach where a formalized specification (requirements, acceptance criteria, invariants) becomes the central artifact of the project. It's versioned alongside the code and serves as the reference for design, implementation, and testing. What makes an approach truly Spec-Driven isn't the presence of a "requirements document". It's three principles: - **The spec acts as a contract:** if an implementation deviates from it, that's a defect, not a valid variant. - **Every decision is traceable:** a component answers a requirement, a test checks an acceptance criterion, a validation protects an invariant. - **The spec is alive:** it evolves at the same pace as the code, versioned in the same repository and reviewed in the same cycle. ## The SDD cycle The cycle is iterative: **Specify → Plan → Develop & Test → Iterate**. The key discipline is separating *what* from *how*: you agree on expected behaviors before debating the best way to implement them. This prevents architecture from accidentally dictating the product. ## SDD vs BDD vs PDD - **BDD** formalizes behavior through tests (Given/When/Then). SDD structures the entire project around the spec. Both can coexist. - **PDD** (Prompt-Driven Development) optimizes the exchange with AI via prompts: effective for prototyping, but the prompt is ephemeral. SDD uses prompts as a tool while the spec remains the reference. ## What AI changes Tools like **Kiro** now allow you to organize work around **Requirements → Design → Tasks**, making the spec an active artifact rather than a static document. Modern AI can: - transform a product intention into structured requirements - derive a coherent design from those requirements - generate implementation tasks - produce tests aligned with acceptance criteria - verify that code still honors the original intent The specification stops being a burden and becomes an accelerator. Read the full article on the Ippon blog: [Spec-Driven Development: From Specification to Code with AI](https://blog.ippon.fr/2026/02/18/spec-driven-development/) --- # Building a Blazing-Fast Portfolio with Astro and Deploying with Netlify URL: https://djamel-bougouffa.com/blog/building-a-blazing-fast-portfolio-with-astro-and-deploying-with-netlify/ Date: 2023-07-24 Tags: Front-End, Software Engineering > How to create a lightning-fast portfolio using Astro with the Portfolio theme and deploy it effortlessly with Netlify. ## Introduction In this article, we'll explore how to create a lightning-fast portfolio using Astro with the Portfolio theme and then deploy it effortlessly using Netlify. With this powerful combination, you can quickly get your portfolio up and running to impress potential clients and employers. ## What is Astro? Astro is a modern static site generator that combines the best features of traditional static site generators with the power of contemporary frameworks. Its unique approach compiles your website into a fully optimized JavaScript application, resulting in exceptional performance and a seamless user experience. Additionally, Astro supports popular frameworks like React, Vue, and Svelte, allowing developers to create complex web applications with ease. ## Introducing the Portfolio Theme This theme includes essential features such as project sections. It provides an excellent foundation for you to showcase your work and achievements effectively. ## Getting Started with Astro and the Portfolio Theme Follow these steps to create your fast portfolio using Astro and the Portfolio theme: ### Step 1: Set up your development environment Ensure you have Node.js and npm installed on your system. You can check their presence by running the following commands in your terminal: ```bash node -v npm -v ``` ### Step 2: Create a new Astro project with template portfolio Open your terminal and create a new Astro project by running the following commands: ```bash npm create astro@latest -- --template portfolio ``` ### Step 3: Customize your portfolio Replace the default content in the `src` directory with your own projects, images, and information. You can easily modify the Portfolio theme's components and layouts to match your style and preferences. ### Step 4: Test your portfolio locally To view your portfolio locally, run the development server: ```bash npm run dev ``` Your portfolio should now be accessible at `http://localhost:3000`. ## Deploying with Netlify Netlify makes the deployment process incredibly simple, providing a seamless experience for deploying static sites. Follow these steps to deploy your Astro-powered portfolio with Netlify: ### Step 1: Create a Netlify account If you don't already have one, sign up for a free account on Netlify at [netlify.com](https://www.netlify.com/). ### Step 2: Install Netlify CLI Install the Netlify CLI globally on your machine to interact with Netlify from the command line: ```bash npm install netlify-cli -g ``` ### Step 3: Build your portfolio Before deploying, build your Astro project to generate the optimized files: ```bash npm run build ``` ### Step 4: Deploy to Netlify Navigate to your project directory in the terminal and run: ```bash netlify init ``` Follow the prompts to link your Netlify account and select the project you want to deploy. ### Step 5: Deploy your portfolio Finally, deploy your portfolio with the following command: ```bash netlify deploy --prod ``` Your portfolio will be automatically deployed to Netlify, and you will receive a unique URL where your blazing-fast portfolio can be accessed by anyone, anywhere. ## Conclusion Creating a fast and impressive portfolio has never been easier with Astro and the Portfolio theme. Astro's performance-oriented approach and seamless integration with modern frameworks allow you to build a top-tier portfolio effortlessly. Additionally, deploying your portfolio with Netlify ensures that it is accessible to the world with just a few clicks. So, why wait? Create your dazzling portfolio with Astro and the Portfolio theme and showcase your skills to the global tech community today! --- # Full-Remote Bliss: Fullstack Dev's Jet Lag Advantage URL: https://djamel-bougouffa.com/blog/full-remote-bliss-fullstack-devs-jet-lag-advantage/ Date: 2023-06-21 Tags: Business & Trends > How working remotely across time zones can transform jet lag from an inconvenience into a productivity and personal growth advantage. ## About my experience In today's digital age, the concept of work has evolved significantly, transcending traditional office spaces. Remote work has gained immense popularity, offering professionals the freedom to work from anywhere in the world. As a fullstack developer who has fully embraced the remote lifestyle, I have discovered a unique advantage that comes with this flexibility: leveraging jet lag to enhance productivity and personal growth. In this article, I will share my personal experience as a fullstack developer, highlighting the advantages of working remotely and how jet lag can be harnessed as a catalyst for success. ## The Freedom of Full-Remote Work One of the most enticing aspects of working as a fullstack developer in a remote setting is the freedom it offers. By eliminating the constraints of a physical office, I have the flexibility to work from any location and at any time. This level of autonomy allows me to design my work environment according to my preferences and achieve a healthy work-life balance. Whether it's working from the comfort of my home, a bustling café, or a serene beach, the ability to choose where and how I work has significantly enhanced my overall well-being and job satisfaction. ## Unlocking the Power of Jet Lag Jet lag, typically considered an inconvenience associated with long-distance travel, can actually be transformed into an advantage for remote workers. When working across different time zones, jet lag can become a powerful tool to increase productivity and personal growth. ### Utilizing Time Zones to Maximize Efficiency The beauty of remote work lies in the opportunity to collaborate with colleagues or clients located in different parts of the world. By strategically planning my work schedule to align with the time zones of my team, I can optimize communication and collaboration. For example, if I'm based in Europe and working with a team in Asia, I can leverage the time difference to complete tasks during my local morning hours while they are asleep. This allows me to provide valuable updates and progress reports before they even start their day, fostering efficiency and seamless workflow. ### Enhanced Focus and Concentration Jet lag can disrupt our regular sleep patterns, but it can also create unique opportunities for enhanced focus and concentration. By leveraging the quiet early morning or late-night hours, when most people are still asleep, I can immerse myself in deep work without distractions. This uninterrupted time has proved to be incredibly valuable for tackling complex coding challenges, debugging, and engaging in creative problem-solving. ### Personal Growth and Cultural Exposure Working remotely across different time zones exposes us to diverse cultures and perspectives. As a fullstack developer, this has been an enriching experience, allowing me to broaden my understanding of global markets, user behaviors, and technological advancements. Engaging with colleagues from various time zones fosters cross-cultural collaboration, expands professional networks, and opens doors to unique learning opportunities that would be unlikely in a traditional office setting. ## Maintaining Balance and Mitigating Challenges While the advantages of full-remote work and jet lag optimization are substantial, it's crucial to maintain a healthy work-life balance and mitigate potential challenges. ### Establishing a Routine To optimize productivity and mitigate the negative effects of jet lag, establishing a consistent routine is essential. Designing a schedule that aligns with both personal and professional commitments ensures a sense of structure, promoting work efficiency and overall well-being. ### Prioritizing Self-Care Remote work can blur the boundaries between work and personal life, making it crucial to prioritize self-care. Taking breaks, engaging in physical activity, and maintaining social connections are vital for mental and physical well-being. When dealing with jet lag, ensuring adequate rest and proper sleep hygiene become even more crucial to avoid burnout. ### Effective Communication and Collaboration Remote work heavily relies on effective communication and collaboration. Embracing digital tools, such as project management platforms, instant messaging, and video conferencing, is vital for maintaining seamless interactions with colleagues across different time zones. Clear and transparent communication channels help establish expectations, ensure alignment, and foster a cohesive team environment. ## Conclusion As a fullstack developer who has fully embraced the full-remote lifestyle, the advantages have been transformative. The flexibility to work from anywhere, coupled with the strategic utilization of jet lag, has allowed me to unlock higher productivity levels, gain exposure to diverse cultures, and achieve a healthier work-life balance. By maintaining routine, prioritizing self-care, and fostering effective communication, the full-remote journey can be a remarkable experience that enhances personal growth and professional success. With the world increasingly embracing remote work, leveraging the advantages of full-remote and jet lag optimization can prove to be a game-changer for developers and professionals across various industries. ---