This page is for anyone facing an ABAP round, from a first developer job to a senior technical lead. Most ABAP interviews start with the data dictionary and internal tables, move to SELECT performance, modularisation and reports, then test how you move data with BDC, BAPIs, IDocs and RFC, and how you change standard behaviour with exits and BADIs. Newer rounds add CDS views, AMDP and object-oriented ABAP. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own projects.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Transparent: one dictionary table maps to one database table with the same name and fields.
Pooled and cluster: many logical tables stored together in one physical table; cluster rows are grouped by key and compressed.
On HANA: when a system moves to the HANA database most of them are converted into ordinary transparent tables, and S/4HANA builds on that.
"A transparent table is the normal case: what I define in SE11 exists one-to-one in the database, same name, same fields, so any SQL can read it. Pooled tables were a way to store lots of small tables, usually customizing, inside one physical table pool. Cluster tables stored a few related tables together in one physical table cluster, grouped by the shared key and compressed, like the old accounting line items. The catch was that the database didn't know about the logical tables, so you couldn't read them with native SQL, and Open SQL joins on them weren't allowed. On HANA that trade-off stopped making sense, so when a system moves to the HANA database most pooled and cluster tables are turned into plain transparent tables. For me as a developer that mostly means I can join them and push logic down like any other table."
Saying cluster tables are just bigger transparent tables, or not knowing that the database cannot see the logical tables inside a pool or cluster.
Domain: the technical side: data type, length, decimals, allowed values and conversion routine.
Data element: the meaning: field labels, F1 documentation and search help, built on a domain or a built-in type.
Why split: one domain can back many data elements that share a format but mean different things.
"The domain is the technical definition. It says the field is, say, CHAR 10, how many decimals it has, which fixed values or value table are allowed, and whether there's a conversion routine, like the one that adds leading zeros to a customer number. The data element sits on top and carries the meaning: the short, medium and long field labels, the F1 documentation and a search help if there is one. A table field then points to the data element. They're separate so the format can be reused. A single domain for a ten-character partner number can back one data element called sold-to party and another called ship-to party. Both have the same technical shape, but on screen each shows its own label and help text. If the format ever changes, I change it once in the domain."
Mixing the two up, for example saying field labels live on the domain, or saying they are just two names for the same thing.
Create: define a lock object in SE11 on the table and its key fields; activation generates the enqueue and dequeue function modules.
Use: call the enqueue module before reading for change, check the result, release with dequeue when done.
Modes and limits: exclusive, shared and optimistic modes; the lock only works if every program asks for it; SM12 shows current locks.
"I'd create a lock object in SE11, with a name starting with E, on the custom table and its key fields. Activating it generates two function modules, ENQUEUE and DEQUEUE plus the object name. Before my program lets a user change a record, it calls the enqueue module with the key. If someone else holds the lock, it raises the foreign lock exception and I show a message like 'record is being edited by another user'. When the save is done I call the dequeue module, and locks are also released when the program ends. Lock modes matter: exclusive for changing, shared when several people may read but nobody should change, and optimistic for edit screens, where the lock only turns exclusive when the user actually saves. The big limit is that these are logical locks held in the lock table, not database locks. A program that skips the enqueue can still update the row, so every writer must play by the rules. SM12 shows who holds what."
Believing an enqueue locks the database row itself, or never checking the exception after calling the enqueue module.
Standard: index access, non-unique key; a key read is a linear scan unless you sort and use binary search.
Sorted: always kept in key order, unique or non-unique key, key reads use a binary search.
Hashed: unique key only, key reads take about the same time however big the table is, no index access.
"A standard table is a plain list. I can append fast and read by index, but a READ TABLE WITH KEY scans row by row, so on a big table inside a loop it gets slow, unless I sort it and add BINARY SEARCH. A sorted table keeps itself in key order all the time. The key can be unique or not, reads by key use a binary search, and I can still read by index, but an APPEND has to respect that order, while INSERT puts the row in the right place for me. A hashed table needs a unique key and gives near constant-time lookups no matter how many rows, but there's no index access. So I choose by access pattern. If I'll mostly loop over everything, standard is fine. If I'll look up by a unique key many times, like a customer master buffer, hashed. If I need key lookups and also ordered processing or partial key reads, sorted."
Saying hashed tables are always fastest, or using BINARY SEARCH on a standard table that was never sorted by the same key.
Work area: INTO copies each row; changes need an explicit MODIFY back into the table.
Field symbol: ASSIGNING points straight at the row; no copy, and changes write through at once.
Reference and header lines: REFERENCE INTO gives a pointer you can store; header lines are obsolete and not allowed in classes.
"LOOP AT the table INTO a work area copies every row into a separate variable. That's safe, but if I change the work area nothing happens to the table until I write MODIFY, and for wide rows all that copying costs time. LOOP AT ASSIGNING a field symbol doesn't copy anything. The field symbol is a pointer to the actual row, so if I set a field, the table row changes right there. That's faster and it's what I use when updating rows. REFERENCE INTO gives me a data reference, which is useful when I want to keep a pointer to a row after the loop, for example in another table. Two traps with field symbols: after the loop it still points at the last row, and using one that's not assigned gives a short dump, so I check IS ASSIGNED where needed. Header lines I avoid completely. They're obsolete and you can't use them inside classes."
Changing a work area inside the loop and expecting the table to update, or saying field symbols copy the row.
Table expression: itab[ key = value ] reads a row in one expression.
Not found: it raises CX_SY_ITAB_LINE_NOT_FOUND instead of setting sy-subrc; guard with TRY or line_exists.
Constructors: VALUE with FOR and WHERE builds a filtered table in one statement; inline DATA declares on first use.
"The old way is READ TABLE with a key into a work area, then check sy-subrc before using it. In newer syntax I write the table name with square brackets and the key inside, and I can even take a single field off it in the same expression. The big behaviour change is the not-found case. A table expression doesn't set sy-subrc. If there's no matching row it raises CX_SY_ITAB_LINE_NOT_FOUND, so I either wrap it in TRY and CATCH, or check line_exists first when a miss is normal. For the filter, instead of a loop with an IF and an APPEND, I use the VALUE constructor with FOR and a WHERE condition, which builds the new table in one statement. Inline DATA declarations save the separate declaration line. I don't rewrite old code just for style, but in new code it's shorter and the intent is clearer."
" Classic
READ TABLE lt_customers INTO ls_customer
WITH KEY kunnr = lv_kunnr.
IF sy-subrc = 0.
lv_name = ls_customer-name1.
ENDIF.
" 7.40 style
TRY.
lv_name = lt_customers[ kunnr = lv_kunnr ]-name1.
CATCH cx_sy_itab_line_not_found.
CLEAR lv_name.
ENDTRY.
" Filter into a new table in one statement
DATA(lt_open) = VALUE ty_order_tab(
FOR ls_order IN lt_orders WHERE ( status = 'O' ) ( ls_order ) ).
Checking sy-subrc after a table expression, as if it behaved like READ TABLE.
Never in a loop: a SELECT per row means one database round trip per row.
FOR ALL ENTRIES traps: an empty driver table ignores the WHERE and reads everything; duplicate result rows are removed.
Joins: usually better when both tables are in the same database, and the natural choice on HANA.
"A SELECT inside a LOOP is the first thing I look for in slow code, because it's one database trip per row. The fix is to read everything in one go, either with a JOIN or with FOR ALL ENTRIES, and then read the results from a sorted or hashed table in memory. FOR ALL ENTRIES has two traps I always guard against. If the driver table is empty, the database doesn't filter at all and reads every row, so I always check the table isn't initial first. And it removes duplicate rows from the result, so if my field list doesn't include the full key, I can silently lose rows, like two items with the same amount. I also delete duplicates from the driver table before the select. These days, and especially on HANA, I prefer a JOIN or a CDS view, because the database does the work in one statement. FOR ALL ENTRIES stays useful when the driver data came from somewhere other than the database."
Not knowing that an empty driver table returns the whole database table, or leaving key fields out of the field list.
SELECT SINGLE: meant for the full primary key; returns exactly that one row; no ORDER BY.
UP TO 1 ROWS: reads matching rows and stops at one; you can add ORDER BY to decide which one.
Partial key: SELECT SINGLE then returns an arbitrary match, which is usually a hidden bug.
"SELECT SINGLE is for when I know the full primary key, so there can only be one row. It reads that row and that's it, and it doesn't allow ORDER BY. UP TO 1 ROWS is for when several rows could match and I want just one, and usually I care which one, so I add ORDER BY. For example, the latest price record for a material ordered by date, descending. Where people go wrong is using SELECT SINGLE with only part of the key. It works, but which row comes back isn't defined, and the code check will warn about it. On HANA especially, you can't rely on any default order. So I don't pick based on the old argument about which one is faster. I pick based on meaning: full key, SELECT SINGLE; any one of several, or the first by some order, UP TO 1 ROWS with ORDER BY."
Using SELECT SINGLE with a partial key to get 'the latest' record and assuming the database returns it in date order.
Situation: what was slow, who it hurt, and how slow.
Measure: SQL trace in ST05 and runtime analysis in SAT to find the real hot spot.
Fix and proof: what you changed, how you compared before and after with the same data.
"At my last company a nightly billing report had grown to about four hours and was running into the morning, so the finance team started the day without their data. I didn't guess. I ran it for a smaller date range with the SQL trace in ST05 and runtime analysis in SAT. The SQL trace showed thousands of identical-looking selects on the item table, one per header, and SAT showed a nested loop over two big standard tables that took most of the ABAP time. So I replaced the select in the loop with one join, read the items into a sorted table, and swapped the nested loop for a keyed read. I compared the output line by line with the old version on the same data before moving it. The job came down to under twenty minutes. Afterwards I added the trace step to our code review checklist for any report touching large tables."
Saying you 'optimised the code' without any measurement, or changing things until it felt faster.
Subroutine: FORM and PERFORM, local to one program, now obsolete for new code.
Function module: lives in a function group, has a defined interface, can be tested in SE37, can be RFC-enabled or an update module.
Method: part of a class; gives encapsulation, class-based exceptions and ABAP Unit tests. The default for new work.
"A subroutine is a FORM called with PERFORM. It belongs to one program, its parameters are loosely typed in old code, and SAP marks it obsolete, but I still meet thousands of them in legacy reports. A function module lives in a function group, has a proper interface with importing, exporting, changing and tables parameters and exceptions, and I can test it on its own in SE37. It's also what you need for remote calls and for update tasks, so it's still very much alive. One thing to watch is that all function modules in a group share the group's global data. A method belongs to a class. It gives me real encapsulation, class-based exceptions, inheritance and interfaces, and I can write ABAP Unit tests against it. So for new code I write classes and methods. I only create a function module when I need RFC, an update module, or an API someone else expects in that form."
Saying function modules are obsolete, or not knowing that RFC and update tasks still need them.
Register: the call is only recorded; nothing runs until COMMIT WORK.
Run: at commit an update work process runs all registered modules as one unit; one failure rolls them all back.
V1 and V2: V1 for time-critical business data, V2 for less critical data like statistics, run after V1 succeeds; SM13 shows failures.
"When I call a function module IN UPDATE TASK, it doesn't run. The call and its parameters are just recorded. The module has to be marked as an update module in its attributes, and it can only take importing and tables parameters, since nothing comes back to the caller. When the program reaches COMMIT WORK, an update work process picks up everything registered and runs it as one unit. If any V1 module fails, all of it is rolled back and the user is told the update failed, and a ROLLBACK WORK before the commit throws the registrations away. V1 is for time-critical changes that must post together, like the document itself. V2 is for less critical follow-on data, like statistics, and it only runs after the V1 part succeeds. By default the commit is asynchronous, so the dialog carries on. If I need the data in the database before the next step, I use COMMIT WORK AND WAIT. Failed updates show up in SM13."
Thinking the update module runs at the moment it is called, or not knowing where to find failed updates.
Static check: must be caught or declared in RAISING; the compiler checks it. For errors callers should expect.
Dynamic check: checked at run time; an undeclared one that escapes turns into a runtime error. For errors good code can prevent.
No check: can go anywhere without being declared. For errors almost nobody can handle, like running out of resources.
"All three inherit from CX_ROOT, and the difference is how strictly the exception has to be declared. With CX_STATIC_CHECK, any method that can raise it must either catch it or list it in RAISING, and the syntax check warns me if I don't. I use it for business errors the caller should really think about, like 'customer is blocked'. CX_DYNAMIC_CHECK is checked at run time instead. The system's own ones, like dividing by zero, are here, because good code avoids them rather than catching them everywhere. If one escapes a method that doesn't declare it, it becomes a CX_SY_NO_HANDLER dump. CX_NO_CHECK can travel anywhere without being declared, which suits things like resource errors. For my own application exceptions I usually base them on CX_STATIC_CHECK, give them texts through a message class, and pass the original exception as previous when I wrap one, so the log still shows the root cause."
Catching CX_ROOT everywhere and throwing the exception away, or not knowing that undeclared exceptions can end in a dump.
Protect first: pin today's behaviour with ABAP Unit tests or saved outputs before changing anything.
Design: an interface for the country logic, one class per country, and a factory that picks the class by country code.
Scope: build the new countries this way, move the worst old branches gradually, and agree the rest with the lead.
"I'd add the two countries, but not as two more WHEN branches, because that's exactly what keeps breaking. Before I touch anything, I'd capture what the program does today: ABAP Unit tests for the calculation if I can separate it, or at least saved outputs for a set of documents per country. Then I'd define an interface with one method, say calculate, and write a class per country that implements it. A small factory method takes the country code and returns the right object, so the main program only talks to the interface and doesn't care which country it has. The two new countries go in as new classes. I'd move the old branches over gradually, starting with the ones that break most, and rerun the tests each time. If other teams need to add countries without touching our code, a filtered BADI does the same job. And I'd tell the lead up front how much refactoring fits in this deadline."
INTERFACE lif_tax_rule.
METHODS calculate
IMPORTING is_doc TYPE ty_doc
RETURNING VALUE(rv_tax) TYPE ty_amount.
ENDINTERFACE.
" One class per country implements lif_tax_rule.
" The factory is the only place that knows the list.
CLASS lcl_rule_factory DEFINITION.
PUBLIC SECTION.
CLASS-METHODS get
IMPORTING iv_country TYPE land1
RETURNING VALUE(ro_rule) TYPE REF TO lif_tax_rule.
ENDCLASS.
" The caller never knows which class it got:
" lv_tax = lcl_rule_factory=>get( lv_land1 )->calculate( ls_doc ).
Adding two more WHEN branches because it's quicker, or rewriting the whole program under a deadline with no tests to prove nothing changed.
Before the screen: LOAD-OF-PROGRAM, then INITIALIZATION for default values.
Selection screen: AT SELECTION-SCREEN OUTPUT to change the screen, AT SELECTION-SCREEN to validate input.
Processing and output: START-OF-SELECTION for the main logic, then END-OF-SELECTION; TOP-OF-PAGE and END-OF-PAGE fire whenever a list page starts or ends.
"First LOAD-OF-PROGRAM runs when the program is loaded, which I rarely use in a report. Then INITIALIZATION, before the selection screen appears, where I set defaults, like the posting date to the start of the month. AT SELECTION-SCREEN OUTPUT runs every time the screen is shown, so that's where I hide or grey out fields depending on a radio button. When the user presses execute, AT SELECTION-SCREEN runs, which is where I validate input and raise an error message so the user stays on the screen to fix it. There are variants too, like ON VALUE-REQUEST for a custom F4 help. Then START-OF-SELECTION, where the main logic goes: read data, process it. It's also the default event if I write code without any event keyword. END-OF-SELECTION comes after. TOP-OF-PAGE fires with the first output on each new page for headers, and END-OF-PAGE for footers if I've reserved lines for them."
Putting input validation in START-OF-SELECTION, or not knowing which event runs before the selection screen appears.
Lists: the basic list is level 0; each drill-down builds a secondary list, up to 20 levels, tracked in sy-lsind.
Events: AT LINE-SELECTION on double-click, AT USER-COMMAND for custom buttons, TOP-OF-PAGE DURING LINE-SELECTION for headers.
Which line: HIDE stores values with each written line; GET CURSOR reads the field under the cursor.
"The first output is the basic list, level zero. When the user double-clicks a line, AT LINE-SELECTION fires, and whatever I write in that event becomes a secondary list on top. sy-lsind tells me which level I'm on, and it can go to 20 secondary lists. To know which record was clicked, the classic way is HIDE. After I write each line, say a sales order, I HIDE the order number, and the system stores it against that line. When the user double-clicks, it puts the value back into my variable before AT LINE-SELECTION runs, so I just read the items for that order. GET CURSOR FIELD is the other option when I need to know which column was clicked. For my own toolbar buttons I set a GUI status and handle AT USER-COMMAND, and for headers on the drill-down lists I use TOP-OF-PAGE DURING LINE-SELECTION. For new reports I'd do the same thing with an ALV and a double-click handler."
Reading the clicked line by counting rows yourself instead of using HIDE, GET CURSOR or sy-lisel.
REUSE function: old but everywhere; built from a field catalogue; fine for simple read-only lists.
CL_SALV_TABLE: object model, very little code, columns from the table type; not meant for editable cells.
CL_GUI_ALV_GRID: needs a container on a screen, but gives editable cells, events and toolbar control.
"REUSE_ALV_GRID_DISPLAY is the function module most older reports use. I build a field catalogue, pass the table and it shows a grid. It still works, but I wouldn't start new reports with it. CL_SALV_TABLE is my default for a read-only report. I call the factory method with my table, it works out the columns from the structure, and I adjust things like column texts, sorting and totals through its objects. It's very little code, but it's not meant for editing cells. When users need to edit data in the grid, or I need full control of the toolbar and events like data changed, I use CL_GUI_ALV_GRID. That one needs a screen with a custom container, or a docking container, so there's more setup, and I handle the events through a handler class. On HANA with very large tables, there's also the IDA version of the ALV, which pushes paging and sorting down to the database instead of loading everything into memory."
Trying to force editable cells into CL_SALV_TABLE, or not knowing the grid control needs a container.
SmartForms: designed in the SMARTFORMS transaction; generates a function module whose name differs per system.
Adobe Forms: interface and form in SFP, layout in Adobe's designer, needs Adobe Document Services to render PDF.
Driver: get the generated name by form name, then call it; Adobe also needs job open and close calls.
"SmartForms are built in the SMARTFORMS transaction, with pages, windows, text and table nodes, all inside SAP. Adobe Forms are built in SFP. I define an interface for the data and then design the layout in Adobe's form designer, and the output is a PDF, which can also be an interactive form the user fills in. The catch with Adobe is that it needs Adobe Document Services running to render the PDF, so there's a basis dependency. In both cases activating the form generates a function module, and its name isn't the same in every system. So in the driver program I never hard-code it. For SmartForms I call SSF_FUNCTION_MODULE_NAME with the form name and then call whatever name comes back. For Adobe I get the name with FP_FUNCTION_MODULE_NAME, open the print job with FP_JOB_OPEN, call the generated module, then close it with FP_JOB_CLOSE. The driver reads the data, the form only lays it out."
Hard-coding the generated function module name in the driver program.
Exits: user exits are FORM routines in SAP includes; customer exits are SMOD and CMOD function, screen and menu exits.
BADIs: object-oriented hooks; defined in SE18, implemented in SE19; can be multiple use and filtered; newer kernel BADIs live in enhancement spots.
Enhancement points: implicit ones at the start and end of many units, explicit ones placed by SAP; use them when no BADI fits.
"User exits are the oldest: empty FORM routines in SAP's own includes, like in sales order processing. Technically that's changing an SAP object, so it needs an access key. Customer exits are planned hooks managed in SMOD and CMOD, mainly function exits called with CALL CUSTOMER-FUNCTION, plus screen and menu exits. You activate them through a project, and only one project can use each one. BADIs are the object-oriented version: SAP defines an interface, I implement it in a class in SE19. A BADI can be multiple use, so several implementations run, and it can have a filter, like one implementation per country. The newer kernel BADIs sit in enhancement spots and are faster. The enhancement framework adds implicit enhancement points at the start and end of many routines, and explicit points SAP placed on purpose. My order is: a BADI first, then a customer exit, then an explicit and finally an implicit enhancement. Changing standard code directly comes last."
Going straight to modifying standard code, or saying all four are the same thing with different names.
Understand: the business rule and exactly where in the process it must apply.
Look for a hook: SAP notes, BADIs, customer exits, then explicit and implicit enhancement points.
Decide and record: modification only as a last resort, agreed and documented, knowing it comes back at every upgrade.
"I wouldn't just edit the line. First I'd sit with the consultant and get the actual rule: what should be blocked, for which document types, and with what message. Often there's a configuration option or an SAP note that already covers it. If not, I look for a proper hook near that point: a BADI, a customer exit, or an explicit enhancement point. I'd find them by debugging the transaction with breakpoints on the BADI and exit calls, and by checking the package of the program. If none fits, an implicit enhancement at the start or end of the routine usually works. A direct modification needs an access key, it shows up for adjustment in SPAU at every upgrade, and it's easy to lose track of. So if we really have no choice, I'd agree it with the lead, document why, and keep the change as small as possible."
Getting an access key and editing the standard program without looking for an enhancement or telling anyone.
BDC: replays screen entries from a recording, so it breaks when screens change.
BAPI: a released interface that posts through business logic, no screens, errors come back in a return table.
Call transaction vs session: call transaction runs now and you handle errors; a session runs later from SM35 with an automatic log.
"BDC simulates a user. I record the transaction in SHDB, and the program fills a BDCDATA table with screen names, field names and values, then plays it back. It works for almost any transaction, but it depends on the screens, so a new field, a changed layout or a different user setting can break it. A BAPI is a released interface for a business object. It skips the screens, runs the business checks and gives me messages in a return table, and it stays stable across upgrades. So if a BAPI exists, I use it. Within BDC, call transaction runs immediately, in modes like all screens, errors only or no display. It's fast, but I have to collect the messages and handle errors myself. The session method writes the data into a batch input session that someone processes in SM35 later, and it keeps a log and lets you reprocess the failed ones."
Choosing BDC when a suitable BAPI exists, or not knowing how errors are captured in the call transaction method.
Check return: read the BAPIRET2 messages; types E and A mean failure.
Commit: call BAPI_TRANSACTION_COMMIT, usually with WAIT, only if there were no errors.
Rollback: otherwise call BAPI_TRANSACTION_ROLLBACK and log the messages.
"Most likely I forgot the commit. Most BAPIs deliberately don't commit, so the caller can group several calls into one unit. The BAPI can run happily, even hand back a document number, and without a commit nothing reaches the database. The second thing is error handling. BAPIs don't raise exceptions for business errors. They fill a RETURN table of type BAPIRET2, so I check it for messages of type E or A. If there are none, I call BAPI_TRANSACTION_COMMIT with WAIT set, so the update has finished before my next step reads the document. If there are errors, I call BAPI_TRANSACTION_ROLLBACK and log the messages for the user. In a mass load I also make sure one bad record's rollback doesn't throw away the good ones, so I commit or roll back per document, not once at the end."
CALL FUNCTION 'BAPI_SALESORDER_CREATEFROMDAT2'
EXPORTING
order_header_in = ls_header
IMPORTING
salesdocument = lv_vbeln
TABLES
return = lt_return
order_items_in = lt_items
order_partners = lt_partners.
IF line_exists( lt_return[ type = 'E' ] )
OR line_exists( lt_return[ type = 'A' ] ).
CALL FUNCTION 'BAPI_TRANSACTION_ROLLBACK'.
ELSE.
CALL FUNCTION 'BAPI_TRANSACTION_COMMIT'
EXPORTING
wait = abap_true.
ENDIF.
Checking sy-subrc after the BAPI call and assuming success, or calling COMMIT WORK after errors were returned.
Structure: one control record, data records made of segments, and status records; defined by message type and basic type.
Setup: logical systems, RFC destination, port, partner profile, and a distribution model for ALE.
Monitor: WE02 or WE05 to view, BD87 to reprocess; inbound 53 is posted, 51 is an application error; outbound 03 means handed to the port.
"An IDoc has three kinds of records. The control record says who sends it, who receives it, the message type, like ORDERS or MATMAS, and the basic type, which is the segment layout. The data records hold the business data in segments, and if I need custom fields I add an extension with my own segment. The status records log each step. To send one I need logical systems for both sides, an RFC destination in SM59, a port in WE21, a partner profile in WE20 that links the partner and message type to the port or the process code, and for ALE a distribution model in BD64. When something fails I open it in WE02 or WE05. For inbound, 53 means the document posted, 51 means the application rejected it, and the status record carries the actual error, like a missing material. Outbound, 03 means it was passed to the port. After fixing the cause, I reprocess in BD87."
Reprocessing failed IDocs again and again without reading the status message or fixing the root cause.
Synchronous: CALL FUNCTION ... DESTINATION; the caller waits for the answer.
Asynchronous: STARTING NEW TASK; runs in parallel, results collected by a callback.
Transactional and queued: IN BACKGROUND TASK, run once after COMMIT WORK; queued adds a fixed order. Monitor in SM58 and the queue monitors.
"All of them call a remote-enabled function module through a destination set up in SM59. The module has to pass its parameters by value, since there's no shared memory across systems. Synchronous RFC is the simple one: I call the function with DESTINATION and wait for the result, and I handle the system failure and communication failure exceptions. Asynchronous RFC uses STARTING NEW TASK. My program carries on and I collect the result later in a callback, which is also a common way to run work in parallel. Transactional RFC, IN BACKGROUND TASK, doesn't run straight away. The calls are stored and sent when I COMMIT WORK, and the system makes sure each one runs exactly once, retrying if the target is down. Failures show up in SM58. Queued RFC adds ordering, so if document changes must arrive in sequence, like creates before updates, I put them in a queue. Newer systems use background RFC for both."
Using synchronous RFC for something that must not be lost if the other system is down, with no retry or monitoring.
Contain: how you noticed, how big it was, who you told.
Diagnose: status records, sample IDocs, what had changed recently.
Recover and prevent: fix, reprocess in a controlled way, then add monitoring or validation.
"In one support role, a Monday morning started with several hundred inbound order IDocs in status 51 and customer service asking where the orders were. I let the business know straight away that orders were received but not posted, so they didn't ask customers to resend. Then I opened a few in WE02. The status message was the same on all of them: a unit of measure the system didn't recognise. The sending system had changed a code over the weekend. I agreed with the functional team to add a conversion rule rather than edit hundreds of IDocs by hand, tested it on one IDoc in quality, then reprocessed the rest in batches through BD87, checking that no order got posted twice. Afterwards we added a daily alert for IDocs in error and an agreement that the partner tells us before changing codes."
Mass reprocessing IDocs without finding the cause first, or never telling the business what was going on.
What it is: a view defined in DDL source in Eclipse-based tools, run inside the database and read with Open SQL.
Beyond SE11: outer joins, unions, calculations, CASE, aggregations, parameters and associations.
Annotations and access: annotations drive analytics, UI and services; access control roles filter rows by authorisation.
"A CDS view is a view I write as source code in the Eclipse tools, and it's executed in the database. I read it with an ordinary SELECT from ABAP. The idea is code pushdown: instead of pulling rows into ABAP and looping over them, the database does the joining, filtering and calculating, which is where HANA is fast. Compared to an SE11 database view, which is basically inner joins and a field list, a CDS view can do outer joins, unions, CASE expressions, calculations, grouping with aggregates and input parameters. Associations are a big one. I declare the relation once, like order to items, and consumers follow it only when they need it. Annotations add meaning on top, for analytics, for UI and for exposing it as a service, and an access control definition can filter rows by the user's authorisations. On newer releases I write view entities rather than the older kind that also generated an SE11 view."
Describing CDS as just a new place to write the same inner join, with no mention of pushdown or associations.
What: a method of a global class whose body is SQLScript run on HANA; the class implements the HDB marker interface.
How: BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT, listing used tables after USING; parameters by value.
When: only when CDS and Open SQL can't express the logic; costs are HANA lock-in, manual client handling, harder debugging.
"An AMDP is an ABAP managed database procedure. I write a method in a global class that implements the interface IF_AMDP_MARKER_HDB, and in the implementation I add BY DATABASE PROCEDURE FOR HDB LANGUAGE SQLSCRIPT, usually OPTIONS READ-ONLY, and list every table I use after USING. The body is SQLScript, not ABAP, and all parameters are passed by value. ABAP manages it, so it's transported like any class and created in the database when first called. There's also a function version that backs a CDS table function. I only reach for it when CDS and Open SQL can't do the job, like multi-step logic with intermediate results or HANA-specific functions. The costs are real. It only runs on HANA, it skips the table buffer, I have to handle the client field myself because the automatic client filter of Open SQL doesn't apply, and debugging needs the AMDP debugger in Eclipse. So CDS first, AMDP when there's no other way."
Writing AMDPs for simple reads that a CDS view could do, or forgetting to filter by client.
Scope: how much custom code, and how you decided what was still used.
Find: code inspector and ATC checks for HANA and S/4 readiness, plus usage data to skip dead code.
Fix: the typical breaks, like missing ORDER BY and removed or replaced tables, and how you tested.
"I was part of a team moving a system with a lot of custom code to S/4HANA. First we used usage data to find which custom programs were actually run, and we retired a large share that nobody had touched in years. On the rest we ran the ATC with the HANA and S/4 readiness checks. The most common real bug was code that assumed a SELECT came back sorted by key. On the old database it usually did, on HANA it doesn't, so a READ with BINARY SEARCH later on silently missed rows. We added ORDER BY or sorted the table. We also had reads on tables that S/4 replaced with compatibility views, and code that assumed the old length of the material number. For each fix we compared report output from the old and new systems on the same data. Only after that did we start pushing heavy reports down into CDS views."
Saying the migration 'just worked' with no mention of checks, or trying to rewrite every program with no priority.
Breakpoints: session breakpoints, external ones for calls from outside, and breakpoints at a statement or message.
Watchpoints: stop when a variable changes or meets a condition, without knowing where it changes.
Hard places: update debugging for update tasks, JDBG or SM50 for jobs, a small shortcut file dropped on a popup.
"The basic one is /h in the command field, or a breakpoint in the editor. Session breakpoints only work in my GUI session, so when the call comes from outside, like an RFC or a web request, I set an external breakpoint for my user. When I don't know where something happens, I set a breakpoint at a statement, like every MESSAGE or every CALL FUNCTION, or a breakpoint on a specific message. A watchpoint is great when a field gets the wrong value somewhere deep in standard code. It stops the moment the variable changes. For code that runs after COMMIT WORK, I switch on update debugging in the debugger settings. A popup has no command field, so I drag a small text file with a /h command onto it. For a finished background job, I select it in SM37 and type JDBG to rerun it in the debugger. A running one I can debug from SM50."
Only knowing /h and stepping line by line through standard code for an hour.
Workbench: repository objects like programs, classes and tables; not tied to one client.
Customizing: configuration entries; client-specific.
Flow: each developer works in a task; release tasks, then the request; import into quality, test, then import into production.
"A workbench request carries repository objects: programs, classes, function modules, dictionary objects. Those aren't tied to one client. A customizing request carries configuration entries made in the implementation guide, which are client-specific. Each request has tasks, one per developer who touched it, and objects in the request are locked to it until it's released. When my work is ready I release my task, then the request owner releases the request, which exports it to the transport directory. The basis team, or whoever owns it, then imports it into quality through STMS, it gets tested, and later it's imported into production following the transport route. I manage my requests in SE09 or SE10. Anything saved as a local object in the temporary package can't be transported at all, which catches out new developers. And the order of imports matters, because a later import of the same object overwrites the earlier one."
Not knowing that tasks must be released before the request, or that local objects cannot be transported.
See the risk: transporting the program now would carry the colleague's untested change with it.
Options: go back to the production version in version management, apply only the fix, then re-apply the feature; or test the feature first.
Coordinate: agree with the colleague and lead, and control import order so nothing is overwritten.
"The first thing I'd point out is that a transport doesn't carry my few lines. It carries the whole program as it is in development, so if I just add my fix and transport it, the colleague's untested feature goes to production too. So I'd talk to the colleague and the lead right away. The usual safe route is to use version management to compare the current version with what's in production, make sure the colleague's work is saved as a version, restore the production version, apply only the urgent fix, test it in quality and move it. Then the colleague re-applies their feature on top, in a new request. The other option is to speed up testing of their change if it's small, but I wouldn't let urgency push untested code through. I'd also check the import order, because if their old request is imported after mine, it would overwrite my fix in production."
Transporting the program as it is and hoping the colleague's change is harmless.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.